diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /toolkit/components/passwordmgr/test/mochitest | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/passwordmgr/test/mochitest')
107 files changed, 17656 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/mochitest/.eslintrc.js b/toolkit/components/passwordmgr/test/mochitest/.eslintrc.js new file mode 100644 index 0000000000..beb8ec4738 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/.eslintrc.js @@ -0,0 +1,17 @@ +"use strict"; + +module.exports = { + globals: { + promptDone: true, + startTest: true, + // Make no-undef happy with our runInParent mixed environments since you + // can't indicate a single function is a new env. + assert: true, + addMessageListener: true, + sendAsyncMessage: true, + Assert: true, + }, + rules: { + "no-var": "off", + }, +}; diff --git a/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs new file mode 100644 index 0000000000..bc11bb29f8 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/auth2/authenticate.sjs @@ -0,0 +1,216 @@ +function handleRequest(request, response) { + try { + reallyHandleRequest(request, response); + } catch (e) { + response.setStatusLine("1.0", 200, "AlmostOK"); + response.write("Error handling request: " + e); + } +} + +function reallyHandleRequest(request, response) { + let match; + let requestAuth = true, + requestProxyAuth = true; + + // Allow the caller to drive how authentication is processed via the query. + // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar + // The extra ? allows the user/pass/realm checks to succeed if the name is + // at the beginning of the query string. + let query = "?" + request.queryString; + + let expected_user = "", + expected_pass = "", + realm = "mochitest"; + let proxy_expected_user = "", + proxy_expected_pass = "", + proxy_realm = "mochi-proxy"; + let huge = false, + plugin = false, + anonymous = false; + let authHeaderCount = 1; + // user=xxx + match = /[^_]user=([^&]*)/.exec(query); + if (match) { + expected_user = match[1]; + } + + // pass=xxx + match = /[^_]pass=([^&]*)/.exec(query); + if (match) { + expected_pass = match[1]; + } + + // realm=xxx + match = /[^_]realm=([^&]*)/.exec(query); + if (match) { + realm = match[1]; + } + + // proxy_user=xxx + match = /proxy_user=([^&]*)/.exec(query); + if (match) { + proxy_expected_user = match[1]; + } + + // proxy_pass=xxx + match = /proxy_pass=([^&]*)/.exec(query); + if (match) { + proxy_expected_pass = match[1]; + } + + // proxy_realm=xxx + match = /proxy_realm=([^&]*)/.exec(query); + if (match) { + proxy_realm = match[1]; + } + + // huge=1 + match = /huge=1/.exec(query); + if (match) { + huge = true; + } + + // plugin=1 + match = /plugin=1/.exec(query); + if (match) { + plugin = true; + } + + // multiple=1 + match = /multiple=([^&]*)/.exec(query); + if (match) { + authHeaderCount = match[1] + 0; + } + + // anonymous=1 + match = /anonymous=1/.exec(query); + if (match) { + anonymous = true; + } + + // Look for an authentication header, if any, in the request. + // + // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + // + // This test only supports Basic auth. The value sent by the client is + // "username:password", obscured with base64 encoding. + + let actual_user = "", + actual_pass = "", + authHeader, + authPresent = false; + if (request.hasHeader("Authorization")) { + authPresent = true; + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) { + throw new Error("Couldn't parse auth header: " + authHeader); + } + + let userpass = atob(match[1]); + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) { + throw new Error("Couldn't decode auth header: " + userpass); + } + actual_user = match[1]; + actual_pass = match[2]; + } + + let proxy_actual_user = "", + proxy_actual_pass = ""; + if (request.hasHeader("Proxy-Authorization")) { + authHeader = request.getHeader("Proxy-Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) { + throw new Error("Couldn't parse auth header: " + authHeader); + } + + let userpass = atob(match[1]); + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) { + throw new Error("Couldn't decode auth header: " + userpass); + } + proxy_actual_user = match[1]; + proxy_actual_pass = match[2]; + } + + // Don't request authentication if the credentials we got were what we + // expected. + if (expected_user == actual_user && expected_pass == actual_pass) { + requestAuth = false; + } + if ( + proxy_expected_user == proxy_actual_user && + proxy_expected_pass == proxy_actual_pass + ) { + requestProxyAuth = false; + } + + if (anonymous) { + if (authPresent) { + response.setStatusLine( + "1.0", + 400, + "Unexpected authorization header found" + ); + } else { + response.setStatusLine("1.0", 200, "Authorization header not found"); + } + } else if (requestProxyAuth) { + response.setStatusLine("1.0", 407, "Proxy authentication required"); + for (let i = 0; i < authHeaderCount; ++i) { + response.setHeader( + "Proxy-Authenticate", + 'basic realm="' + proxy_realm + '"', + true + ); + } + } else if (requestAuth) { + response.setStatusLine("1.0", 401, "Authentication required"); + for (let i = 0; i < authHeaderCount; ++i) { + response.setHeader( + "WWW-Authenticate", + 'basic realm="' + realm + '"', + true + ); + } + } else { + response.setStatusLine("1.0", 200, "OK"); + } + + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write("<html xmlns='http://www.w3.org/1999/xhtml'>"); + response.write( + "<p>Login: <span id='ok'>" + + (requestAuth ? "FAIL" : "PASS") + + "</span></p>\n" + ); + response.write( + "<p>Proxy: <span id='proxy'>" + + (requestProxyAuth ? "FAIL" : "PASS") + + "</span></p>\n" + ); + response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n"); + response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n"); + response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n"); + + if (huge) { + response.write("<div style='display: none'>"); + for (let i = 0; i < 100000; i++) { + response.write("123456789\n"); + } + response.write("</div>"); + response.write( + "<span id='footnote'>This is a footnote after the huge content fill</span>" + ); + } + + if (plugin) { + response.write( + "<embed id='embedtest' style='width: 400px; height: 100px;' " + + "type='application/x-test'></embed>\n" + ); + } + + response.write("</html>"); +} diff --git a/toolkit/components/passwordmgr/test/mochitest/chrome_timeout.js b/toolkit/components/passwordmgr/test/mochitest/chrome_timeout.js new file mode 100644 index 0000000000..25a797e1d2 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/chrome_timeout.js @@ -0,0 +1,14 @@ +/* eslint-env mozilla/chrome-script */ + +const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); +addMessageListener("setTimeout", msg => { + timer.init( + _ => { + sendAsyncMessage("timeout"); + }, + msg.delay, + Ci.nsITimer.TYPE_ONE_SHOT + ); +}); + +sendAsyncMessage("ready"); diff --git a/toolkit/components/passwordmgr/test/mochitest/file_history_back.html b/toolkit/components/passwordmgr/test/mochitest/file_history_back.html new file mode 100644 index 0000000000..4e3e071a71 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/file_history_back.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + This page should navigate back in history upon load. + <script> + window.onload = function goBack() { + window.history.back(); + }; + </script> + </body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/form_basic_bfcache.html b/toolkit/components/passwordmgr/test/mochitest/form_basic_bfcache.html new file mode 100644 index 0000000000..852ff950b6 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/form_basic_bfcache.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script> + var bc = new BroadcastChannel("form_basic_bfcache"); + bc.onmessage = function(event) { + if (event.data == "nextpage") { + location.href = "https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/file_history_back.html"; + } else if (event.data == "close") { + bc.postMessage("closed"); + bc.close(); + window.close(); + SimpleTest.finish(); + } + } + + function is(val1, val2, msg) { + bc.postMessage({type: "is", value1: val1, value2: val2, message: msg}); + } + + function ok(val, msg) { + bc.postMessage({type: "ok", value: val, message: msg}); + } + </script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script> + // Need to use waitForExplicitFinish also in this support file to + // stop SimpleTest complaining about missing checks. + // pwmgr_common.js uses internally some helper methods from SimpleTest. + SimpleTest.waitForExplicitFinish(); + + runChecksAfterCommonInit(); + + onpageshow = async function(pageShow) { + if (!pageShow.persisted) { + // This is the initial page load. + await setStoredLoginsAsync([location.origin, "", null, "autofilled", "pass1", "", ""]); + } else { + await promiseFormsProcessedInSameProcess(); + let uname = document.getElementById("form-basic-username"); + let pword = document.getElementById("form-basic-password"); + checkLoginForm(uname, "autofilled", pword, "pass1"); + } + + bc.postMessage({type: pageShow.type, persisted: pageShow.persisted}); + } + </script> + </head> + <body> + <form id="form-basic"> + <input id="form-basic-username" name="username"> + <input id="form-basic-password" name="password" type="password"> + <input id="form-basic-submit" type="submit"> + </form> + </body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html new file mode 100644 index 0000000000..ab558c4955 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html @@ -0,0 +1,31 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Simple form with username and password fields together in a shadow root with a <form> ancestor --> +<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html --> +<form id="both-fields-together-in-a-shadow-root"> + <!-- username and password inputs generated programmatically below --> + <input id="submit" type="submit"> +</form> + +<script> + const form = document.getElementById("both-fields-together-in-a-shadow-root"); + const submitButton = document.getElementById("submit"); + const wrapper = document.createElement("span"); + wrapper.id = "wrapper-un-and-pw"; + const shadow = wrapper.attachShadow({mode: "closed"}); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + shadow.append(inputEle); + } + submitButton.before(wrapper); +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html new file mode 100644 index 0000000000..19edd12330 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html @@ -0,0 +1,31 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Simple form with username and password fields each in their own shadow root --> +<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html --> +<form id="each-field-its-own-shadow"> + <!-- username and password inputs generated programmatically below --> + <input id="submit" type="submit"> +</form> + +<script> + const form = document.getElementById("each-field-its-own-shadow"); + const submitButton = document.getElementById("submit"); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + const wrapper = document.createElement("span"); + wrapper.id = `wrapper-${field}`; + const shadow = wrapper.attachShadow({mode: "closed"}); + shadow.append(inputEle); + submitButton.before(wrapper); + } +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html new file mode 100644 index 0000000000..225fb4e7f8 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html @@ -0,0 +1,33 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Simple form with form, username and password fields together in a shadow root --> +<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html --> +<span id="wrapper"> + <!-- form and all inputs generated programmatically below --> +</span> + +<script> + const wrapper = document.getElementById("wrapper"); + const shadow = wrapper.attachShadow({mode: "closed"}); + const form = document.createElement("form"); + form.id = "form-and-fields-in-a-shadow-root"; + const submitButton = document.createElement("input"); + submitButton.id = "submit"; + submitButton.type = "submit"; + shadow.append(form); + form.append(submitButton); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + submitButton.before(inputEle); + } +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html new file mode 100644 index 0000000000..876c7d3b85 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html @@ -0,0 +1,34 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Simple form with username and password fields together in nested shadow roots --> +<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html --> +<form id="each-field-its-own-shadow"> + <span id="outer-wrapper"> + <!-- username and password inputs generated programmatically below --> + </span> + <input id="submit" type="submit"> +</form> + +<script> + const submitButton = document.getElementById("submit"); + const innerWrapper = document.createElement("span"); + innerWrapper.id = "inner-wrapper"; + const innerShadow = innerWrapper.attachShadow({mode: "closed"}); + const outerWrapper = document.getElementById("outer-wrapper"); + const outerShadow = outerWrapper.attachShadow({mode: "closed"}); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + innerShadow.append(inputEle); + } + outerShadow.append(innerWrapper); +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html new file mode 100644 index 0000000000..9a844f236a --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html @@ -0,0 +1,38 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Simple form with username and password fields each in their own nested shadow roots --> +<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html --> +<form id="each-field-its-own-shadow"> + <span id="outer-wrapper-username"> + <!-- username input generated programmatically below --> + </span> + <span id="outer-wrapper-password"> + <!-- password input generated programmatically below --> + </span> + <input id="submit" type="submit"> +</form> + +<script> + const submitButton = document.getElementById("submit"); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + const wrapper = document.createElement("span"); + wrapper.id = `wrapper-${field}`; + const shadow = wrapper.attachShadow({mode: "closed"}); + shadow.append(inputEle); + + const outerWrapper = document.getElementById(`outer-wrapper-${field}`); + const outerShadow = outerWrapper.attachShadow({mode: "closed"}); + outerShadow.append(wrapper); + } +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html new file mode 100644 index 0000000000..79481c4a3a --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html @@ -0,0 +1,37 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Simple form with form, username and password fields together in nested shadow roots --> +<!-- This form is based off of toolkit/components/passwordmgr/test/browser/form_basic.html --> +<span id="outer-wrapper"> + <!-- form and all inputs generated programmatically below --> +</span> + +<script> + const outerWrapper = document.getElementById("outer-wrapper"); + const innerWrapper = document.createElement("span"); + innerWrapper.id = "inner-wrapper"; + const innerShadow = innerWrapper.attachShadow({mode: "closed"}); + const outerShadow = outerWrapper.attachShadow({mode: "closed"}); + const form = document.createElement("form"); + form.id = "form-and-fields-in-a-shadow-root"; + const submitButton = document.createElement("input"); + submitButton.id = "submit"; + submitButton.type = "submit"; + innerShadow.append(form); + form.append(submitButton); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + submitButton.before(inputEle); + } + outerShadow.append(innerWrapper); +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html new file mode 100644 index 0000000000..ed261af9aa --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html @@ -0,0 +1,28 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Simple form with username and password fields together in a shadow root --> +<!-- This form is based off of toolkit/components/passwordmgr/test/browser/formless_basic.html --> +<!-- username and password inputs generated programmatically below --> +<input id="submit" type="submit"> + +<script> + const submitButton = document.getElementById("submit"); + const wrapper = document.createElement("span"); + wrapper.id = "wrapper-un-and-pw"; + const shadow = wrapper.attachShadow({mode: "closed"}); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + shadow.append(inputEle); + } + submitButton.before(wrapper); +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html new file mode 100644 index 0000000000..b0ea0dc486 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html @@ -0,0 +1,28 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Simple form with username and password fields each in their own shadow root --> +<!-- This form is based off of toolkit/components/passwordmgr/test/browser/formless_basic.html --> +<!-- username and password inputs generated programmatically below --> +<input id="submit" type="submit"> + +<script> + const submitButton = document.getElementById("submit"); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + const wrapper = document.createElement("span"); + wrapper.id = `wrapper-${field}`; + const shadow = wrapper.attachShadow({mode: "closed"}); + shadow.append(inputEle); + submitButton.before(wrapper); + } +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html new file mode 100644 index 0000000000..a93e5ea752 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html @@ -0,0 +1,30 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Simple form with form, username and password fields together in a shadow root --> +<!-- This form is based off of toolkit/components/passwordmgr/test/browser/formless_basic.html --> +<span id="wrapper"> +</span> +<!-- username, password and submit inputs generated programmatically below --> + +<script> + const wrapper = document.getElementById("wrapper"); + const shadow = wrapper.attachShadow({mode: "closed"}); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + shadow.append(inputEle); + } + const submitButton = document.createElement("input"); + submitButton.id = "submit"; + submitButton.type = "submit"; + shadow.append(submitButton); +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/mochitest.toml b/toolkit/components/passwordmgr/test/mochitest/mochitest.toml new file mode 100644 index 0000000000..86d5d83912 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.toml @@ -0,0 +1,419 @@ +[DEFAULT] +prefs = [ + "signon.rememberSignons=true", + "signon.autofillForms.http=true", + "signon.showAutoCompleteFooter=true", + 'signon.showAutoCompleteImport=""', + "signon.testOnlyUserHasInteractedByPrefValue=true", + "signon.testOnlyUserHasInteractedWithDocument=true", + "network.auth.non-web-content-triggered-resources-http-auth-allow=true", + # signon.relatedRealms.enabled pref needed until Bug 1699698 lands + "signon.relatedRealms.enabled=true", + "signon.usernameOnlyForm.enabled=true", + "signon.usernameOnlyForm.lookupThreshold=100", + "security.webauth.webauthn_enable_softtoken=true", + "security.webauth.webauthn_enable_usbtoken=false", + "security.webauthn.enable_conditional_mediation=true", +] +support-files = [ + "../../../prompts/test/chromeScript.js", + "!/toolkit/components/prompts/test/prompt_common.js", + "../../../satchel/test/parent_utils.js", + "!/toolkit/components/satchel/test/satchel_common.js", + "../blank.html", + "../browser/form_autofocus_js.html", + "../browser/form_basic.html", + "../browser/formless_basic.html", + "../browser/form_cross_origin_secure_action.html", + "../browser/form_same_origin_action.html", + "auth2/authenticate.sjs", + "file_history_back.html", + "form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html", + "form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html", + "form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html", + "form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html", + "form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html", + "form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html", + "formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html", + "formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html", + "formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html", + "multiple_forms_shadow_DOM_all_known_variants.html", + "pwmgr_common.js", + "pwmgr_common_parent.js", + "../authenticate.sjs", +] +skip-if = ["os == 'android'"] # Don't run on GeckoView + +# Note: new tests should use scheme = https unless they have a specific reason not to + +["test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html"] +scheme = "https" +support-files = [ + "slow_image.sjs", + "slow_image.html", +] + +["test_LoginManagerContent_passwordEditedOrGenerated.html"] +scheme = "https" +skip-if = ["os == 'android'"] # password generation + +["test_autocomplete_autofill_related_realms_no_dupes.html"] +skip-if = ["xorigin"] # Bug 1716412 - New fission platform triage +scheme = "https" + +["test_autocomplete_basic_form.html"] +skip-if = [ + "os == 'android'", # autocomplete + "xorigin", # Bug 1716412 - New fission platform triage + "display == 'wayland' && os_version == '22.04'", # Bug 1857071 +] +scheme = "https" + +["test_autocomplete_basic_form_formActionOrigin.html"] +skip-if = [ + "os == 'android'", # android:autocomplete. + "xorigin", # Bug 1716412 - New fission platform triage +] +scheme = "https" + +["test_autocomplete_basic_form_insecure.html"] +skip-if = [ + "os == 'android'", # autocomplete + "xorigin", # Bug 1716412 - New fission platform triage +] + +["test_autocomplete_basic_form_related_realms.html"] +skip-if = ["xorigin"] # Bug 1716412 - New fission platform triage +scheme = "https" + +["test_autocomplete_hasBeenTypePassword.html"] +scheme = "https" +skip-if = ["os == 'android'"] # autocomplete + +["test_autocomplete_highlight.html"] +scheme = "https" +skip-if = ["os == 'android'"] # autocomplete + +["test_autocomplete_highlight_non_login.html"] +scheme = "https" +skip-if = ["os == 'android'"] # autocomplete + +["test_autocomplete_highlight_username_only_form.html"] +scheme = "https" +skip-if = ["os == 'android'"] # autocomplete + +["test_autocomplete_https_downgrade.html"] +scheme = "http" # Tests downgrading +skip-if = [ + "os == 'android'", # autocomplete + "os == 'linux' && debug", # Bug 1554959 + "xorigin", # Bug 1716412 - New fission platform triage +] + +["test_autocomplete_https_upgrade.html"] +scheme = "https" +skip-if = [ + "verify", + "os == 'android'", # autocomplete + "os == 'linux' && debug", # Bug 1554959 for linux debug disable +] + +["test_autocomplete_password_generation.html"] +scheme = "https" +skip-if = [ + "xorigin", + "os == 'android'", # autocomplete +] + +["test_autocomplete_password_generation_confirm.html"] +scheme = "https" +skip-if = ["os == 'android'"] # autocomplete + +["test_autocomplete_password_generation_telemetry.html"] +scheme = "https" +skip-if = [ + "xorigin", + "os == 'android'", # autocomplete +] + +["test_autocomplete_password_open.html"] +scheme = "https" +skip-if = [ + "os == 'android'", # autocomplete + "verify", +] + +["test_autocomplete_sandboxed.html"] +scheme = "https" +skip-if = ["os == 'android'"] # autocomplete + +["test_autocomplete_tab_between_fields.html"] +scheme = "https" +skip-if = [ + "xorigin", + "os == 'android'", # autocomplete +] + +["test_autofill_autocomplete_types.html"] +scheme = "https" +skip-if = ["os == 'android'"] # bug 1533965 + +["test_autofill_different_formActionOrigin.html"] +scheme = "https" +skip-if = ["os == 'android'"] # Bug 1259768 + +["test_autofill_different_subdomain.html"] +scheme = "https" +skip-if = [ + "os == 'android'", # Bug 1259768 + "http3", + "http2", +] + +["test_autofill_from_bfcache.html"] +scheme = "https" +skip-if = ["os == 'android'"] # bug 1527403 +support-files = ["form_basic_bfcache.html"] + +["test_autofill_hasBeenTypePassword.html"] +scheme = "https" + +["test_autofill_highlight.html"] +scheme = "https" +skip-if = ["os == 'android'"] # Bug 1531185 + +["test_autofill_highlight_empty_username.html"] +scheme = "https" + +["test_autofill_highlight_username_only_form.html"] +scheme = "https" + +["test_autofill_https_downgrade.html"] +scheme = "http" # we need http to test handling of https logins on http forms +skip-if = [ + "http3", + "http2", +] + +["test_autofill_https_upgrade.html"] +skip-if = [ + "os == 'android'", # Bug 1259768 + "http3", + "http2", +] + +["test_autofill_password-only.html"] + +["test_autofill_sandboxed.html"] +scheme = "https" +skip-if = ["os == 'android'"] + +["test_autofill_tab_between_fields.html"] +scheme = "https" + +["test_autofill_username-only.html"] + +["test_autofill_username-only_threshold.html"] + +["test_autofocus_js.html"] +scheme = "https" +skip-if = ["os == 'android'"] # autocomplete + +["test_basic_form.html"] + +["test_basic_form_0pw.html"] + +["test_basic_form_1pw.html"] + +["test_basic_form_1pw_2.html"] + +["test_basic_form_2pw_1.html"] + +["test_basic_form_2pw_2.html"] + +["test_basic_form_3pw_1.html"] + +["test_basic_form_honor_autocomplete_off.html"] +scheme = "https" +skip-if = [ + "xorigin", + "os == 'android'", # android:autocomplete. +] + +["test_basic_form_html5.html"] + +["test_basic_form_pwevent.html"] +skip-if = ["xorigin"] + +["test_basic_form_pwonly.html"] + +["test_bug_627616.html"] +skip-if = [ + "os == 'android'", # Tests desktop prompts + "http3", + "http2", +] + +["test_bug_776171.html"] + +["test_case_differences.html"] +skip-if = ["os == 'android'"] # autocomplete +scheme = "https" + +["test_dismissed_doorhanger_in_shadow_DOM.html"] +skip-if = ["os == 'android'"] # Tests desktop prompt +scheme = "https" + +["test_formLike_rootElement_with_Shadow_DOM.html"] +scheme = "https" + +["test_form_action_1.html"] + +["test_form_action_2.html"] + +["test_form_action_javascript.html"] + +["test_formless_autofill.html"] +skip-if = [ + "xorigin", + "http3", + "http2", +] + +["test_formless_submit.html"] +skip-if = [ + "os == 'android' && debug", # bug 1397615 + "http3", + "http2", +] + +["test_formless_submit_form_removal.html"] +skip-if = [ + "http3", + "http2", +] + +["test_formless_submit_form_removal_negative.html"] +skip-if = [ + "http3", + "http2", +] + +["test_formless_submit_navigation.html"] +skip-if = [ + "os == 'android' && debug", # bug 1397615 + "http3", + "http2", +] + +["test_formless_submit_navigation_negative.html"] +skip-if = [ + "os == 'android' && debug", # bug 1397615 + "http3", + "http2", +] + +["test_include_other_subdomains_in_lookup.html"] +skip-if = ["os == 'android'"] # android:autocomplete. +scheme = "https" + +["test_input_events.html"] +skip-if = ["xorigin"] + +["test_input_events_for_identical_values.html"] + +["test_insecure_form_field_no_saved_login.html"] +skip-if = ["os == 'android'"] # android:autocomplete. + +["test_maxlength.html"] + +["test_munged_values.html"] +scheme = "https" +skip-if = ["os == 'android'"] # bug 1527403 + +["test_one_doorhanger_per_un_pw.html"] +scheme = "https" +skip-if = ["os == 'android'"] # bug 1535505 + +["test_onsubmit_value_change.html"] + +["test_password_field_autocomplete.html"] +skip-if = ["os == 'android'"] # android:autocomplete. + +["test_password_length.html"] +scheme = "https" +skip-if = ["os == 'android'"] # bug 1527403 + +["test_passwords_in_type_password.html"] + +["test_primary_password.html"] +scheme = "https" +run-if = ["os == 'mac'"] +skip-if = [ + "verify", + "xorigin", # Tests desktop prompts and bug 1333264 +] +support-files = [ + "chrome_timeout.js", + "subtst_primary_pass.html", +] + +["test_prompt.html"] +skip-if = [ + "os == 'linux'", + "os == 'android'", # Tests desktop prompts +] + +["test_prompt_async.html"] +skip-if = [ + "os == 'android'", # Tests desktop prompts + "http3", + "http2", +] +support-files = ["subtst_prompt_async.html"] + +["test_prompt_http.html"] +skip-if = [ + "os == 'android'", # Tests desktop prompts + "os == 'linux'", + "xorigin", # Bug 1716412 - New fission platform triage +] + +["test_prompt_noWindow.html"] +skip-if = ["os == 'android'"] # Tests desktop prompts. + +["test_prompt_promptAuth.html"] +skip-if = [ + "os == 'linux'", + "os == 'android'", # Tests desktop prompts +] + +["test_prompt_promptAuth_proxy.html"] +skip-if = [ + "os == 'linux'", + "os == 'android'", # Tests desktop prompts +] + +["test_recipe_login_fields.html"] +skip-if = ["xorigin"] + +["test_set_stored_logins_during_task.html"] + +["test_submit_without_field_modifications.html"] +support-files = ["subtst_prefilled_form.html"] +skip-if = [ + "xorigin", + "http3", + "http2", +] + +["test_username_focus.html"] +skip-if = [ + "xorigin", + "os == 'android'", # android:autocomplete. +] + +["test_xhr.html"] +skip-if = ["os == 'android'"] # Tests desktop prompts + +["test_xhr_2.html"] diff --git a/toolkit/components/passwordmgr/test/mochitest/multiple_forms_shadow_DOM_all_known_variants.html b/toolkit/components/passwordmgr/test/mochitest/multiple_forms_shadow_DOM_all_known_variants.html new file mode 100644 index 0000000000..5ba547e671 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/multiple_forms_shadow_DOM_all_known_variants.html @@ -0,0 +1,111 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Page with multiple forms containing the following Shadow DOM variants: --> +<!-- Case 1: Each field (username and password) in its own shadow root --> +<!-- Case 2: Both fields (username and password) together in a shadow root with a form ancestor --> +<!-- Case 3: Form and fields (username and password) together in a shadow root --> +<span id="outer-wrapper"> +</span> + +<script> + const outerWrapper = document.getElementById("outer-wrapper"); + const outerShadow = outerWrapper.attachShadow({mode: "closed"}); + + function makeFormlessOuterForm(scenario) { + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = `${field}-${scenario}`; + inputEle.name = `${field}-${scenario}`; + if (field === "password") { + inputEle.type = field; + } + outerShadow.append(inputEle); + } + const submitButton = document.createElement("input"); + submitButton.id = `submit-${scenario}`; + submitButton.type = "submit"; + outerShadow.append(submitButton); + } + + function makeFormEachFieldInItsOwnShadowRoot(scenario) { + const form = document.createElement("form"); + form.id = scenario; + const submitButton = document.createElement("input"); + submitButton.id = `submit-${scenario}`; + submitButton.type = "submit"; + form.append(submitButton); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = `${field}-${scenario}`; + inputEle.name = `${field}-${scenario}`; + if (field === "password") { + inputEle.type = field; + } + const wrapper = document.createElement("span"); + wrapper.id = `wrapper-${field}-${scenario}`; + const shadow = wrapper.attachShadow({mode: "closed"}); + shadow.append(inputEle); + submitButton.before(wrapper); + } + outerShadow.append(form); + } + + function makeFormBothFieldsTogetherInAShadowRoot(scenario) { + const form = document.createElement("form"); + form.id = scenario; + const submitButton = document.createElement("input"); + submitButton.id = `submit-${scenario}`; + submitButton.type = "submit"; + form.append(submitButton); + const wrapper = document.createElement("span"); + wrapper.id = `wrapper-${scenario}`; + const shadow = wrapper.attachShadow({mode: "closed"}); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = `${field}-${scenario}`; + inputEle.name = `${field}-${scenario}`; + if (field === "password") { + inputEle.type = field; + } + shadow.append(inputEle); + } + submitButton.before(wrapper); + outerShadow.append(form); + } + + function makeFormFormAndFieldsTogetherInAShadowRoot(scenario) { + const wrapper = document.createElement("span"); + wrapper.id = `wrapper-${scenario}`; + const shadow = wrapper.attachShadow({mode: "closed"}); + const form = document.createElement("form"); + form.id = scenario; + shadow.append(form); + const submitButton = document.createElement("input"); + submitButton.id = `submit-${scenario}`; + submitButton.type = "submit"; + form.append(submitButton); + const fields = ["username", "password"]; + for (let field of fields) { + const inputEle = document.createElement("input"); + inputEle.id = field; + inputEle.name = field; + if (field === "password") { + inputEle.type = field; + } + submitButton.before(inputEle); + } + outerShadow.append(wrapper); + } + + makeFormlessOuterForm("formless-case-2"); + makeFormEachFieldInItsOwnShadowRoot("form-case-1"); + makeFormBothFieldsTogetherInAShadowRoot("form-case-2"); + makeFormFormAndFieldsTogetherInAShadowRoot("form-case-3"); +</script> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js new file mode 100644 index 0000000000..8b125897a5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js @@ -0,0 +1,1175 @@ +/** + * Helpers for password manager mochitest-plain tests. + */ + +/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */ + +const { LoginTestUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" +); +const Services = SpecialPowers.Services; + +// Setup LoginTestUtils to report assertions to the mochitest harness. +LoginTestUtils.setAssertReporter( + SpecialPowers.wrapCallback((err, message, stack) => { + SimpleTest.record(!err, err ? err.message : message, null, stack); + }) +); + +const { LoginHelper } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/LoginHelper.sys.mjs" +); + +const { LENGTH: GENERATED_PASSWORD_LENGTH, REGEX: GENERATED_PASSWORD_REGEX } = + LoginTestUtils.generation; +const LOGIN_FIELD_UTILS = LoginTestUtils.loginField; +const TESTS_DIR = "/tests/toolkit/components/passwordmgr/test/"; + +// Depending on pref state we either show auth prompts as windows or on tab level. +let authPromptModalType = SpecialPowers.Services.prefs.getIntPref( + "prompts.modalType.httpAuth" +); + +// Whether the auth prompt is a commonDialog.xhtml or a TabModalPrompt +let authPromptIsCommonDialog = + authPromptModalType === SpecialPowers.Services.prompt.MODAL_TYPE_WINDOW || + (authPromptModalType === SpecialPowers.Services.prompt.MODAL_TYPE_TAB && + SpecialPowers.Services.prefs.getBoolPref( + "prompts.tabChromePromptSubDialog", + false + )); + +/** + * Recreate a DOM tree using the outerHTML to ensure that any event listeners + * and internal state for the elements are removed. + */ +function recreateTree(element) { + // eslint-disable-next-line no-self-assign + element.outerHTML = element.outerHTML; +} + +function _checkArrayValues(actualValues, expectedValues, msg) { + is( + actualValues.length, + expectedValues.length, + "Checking array values: " + msg + ); + for (let i = 0; i < expectedValues.length; i++) { + is(actualValues[i], expectedValues[i], msg + " Checking array entry #" + i); + } +} + +/** + * Check autocomplete popup results to ensure that expected + * *labels* are being shown correctly as items in the popup. + */ +function checkAutoCompleteResults(actualValues, expectedValues, hostname, msg) { + if (hostname === null) { + _checkArrayValues(actualValues, expectedValues, msg); + return; + } + + isnot( + actualValues.length, + 0, + "There should be items in the autocomplete popup: " + + JSON.stringify(actualValues) + ); + + // Check the footer first. + let footerResult = actualValues[actualValues.length - 1]; + is(footerResult, "Manage Passwords", "the footer text is shown correctly"); + + if (actualValues.length == 1) { + is( + expectedValues.length, + 0, + "If only the footer is present then there should be no expectedValues" + ); + info("Only the footer is present in the popup"); + return; + } + + // Check the rest of the autocomplete item values. + _checkArrayValues(actualValues.slice(0, -1), expectedValues, msg); +} + +/** + * Wait for autocomplete popup to get closed + * @return {Promise} resolving when the AC popup is closed + */ +async function untilAutocompletePopupClosed() { + return SimpleTest.promiseWaitForCondition(async () => { + const popupState = await getPopupState(); + return !popupState.open; + }, "Wait for autocomplete popup to be closed"); +} + +function getIframeBrowsingContext(window, iframeNumber = 0) { + let bc = SpecialPowers.wrap(window).windowGlobalChild.browsingContext; + return SpecialPowers.unwrap(bc.children[iframeNumber]); +} + +/** + * Set input values via setUserInput to emulate user input + * and distinguish them from declarative or script-assigned values + */ +function setUserInputValues(parentNode, selectorValues, userInput = true) { + for (let [selector, newValue] of Object.entries(selectorValues)) { + info(`setUserInputValues, selector: ${selector}`); + try { + let field = SpecialPowers.wrap(parentNode.querySelector(selector)); + if (field.value == newValue) { + // we don't get an input event if the new value == the old + field.value += "#"; + } + if (userInput) { + field.setUserInput(newValue); + } else { + field.value = newValue; + } + } catch (ex) { + info(ex.message); + info(ex.stack); + ok( + false, + `setUserInputValues: Couldn't set value of field: ${ex.message}` + ); + } + } +} + +/** + * @param {Function} [aFilterFn = undefined] Function to filter out irrelevant submissions. + * @return {Promise} resolving when a relevant form submission was processed. + */ +function getSubmitMessage(aFilterFn = undefined) { + info("getSubmitMessage"); + return new Promise((resolve, reject) => { + PWMGR_COMMON_PARENT.addMessageListener( + "formSubmissionProcessed", + function processed(...args) { + if (aFilterFn && !aFilterFn(...args)) { + // This submission isn't the one we're waiting for. + return; + } + + info("got formSubmissionProcessed"); + PWMGR_COMMON_PARENT.removeMessageListener( + "formSubmissionProcessed", + processed + ); + resolve(args[0]); + } + ); + }); +} + +/** + * @return {Promise} resolves when a onPasswordEditedOrGenerated message is received at the parent + */ +function getPasswordEditedMessage() { + info("getPasswordEditedMessage"); + return new Promise((resolve, reject) => { + PWMGR_COMMON_PARENT.addMessageListener( + "passwordEditedOrGenerated", + function listener(...args) { + info("got passwordEditedOrGenerated"); + PWMGR_COMMON_PARENT.removeMessageListener( + "passwordEditedOrGenerated", + listener + ); + resolve(args[0]); + } + ); + }); +} + +/** + * Create a login form and insert into contents dom (identified by id + * `content`). If the form (identified by its number) is already present in the + * dom, it gets replaced. + * + * @param {number} [num = 1] - number of the form, used as id, eg `form1` + * @param {string} [action = ""] - action attribute of the form + * @param {string} [autocomplete = null] - forms autocomplete attribute. Default is none + * @param {object} [username = {}] - object describing attributes to the username field: + * @param {string} [username.id = null] - id of the field + * @param {string} [username.name = "uname"] - name attribute + * @param {string} [username.type = "text"] - type of the field + * @param {string} [username.value = null] - initial value of the field + * @param {string} [username.autocomplete = null] - autocomplete attribute + * @param {object} [password = {}] - an object describing attributes to the password field. If falsy, do not create a password field + * @param {string} [password.id = null] - id of the field + * @param {string} [password.name = "pword"] - name attribute + * @param {string} [password.type = "password"] - type of the field + * @param {string} [password.value = null] - initial value of the field + * @param {string} [password.label = null] - if present, wrap field in a label containing its value + * @param {string} [password.autocomplete = null] - autocomplete attribute + * + * @return {HTMLDomElement} the form + */ +function createLoginForm({ + num = 1, + action = "", + autocomplete = null, + username = {}, + password = {}, +} = {}) { + username.name ||= "uname"; + username.type ||= "text"; + username.id ||= null; + username.value ||= null; + username.autocomplete ||= null; + + password.name ||= "pword"; + password.type ||= "password"; + password.id ||= null; + password.value ||= null; + password.label ||= null; + password.autocomplete ||= null; + password.readonly ||= null; + password.disabled ||= null; + + info( + `Creating login form ${JSON.stringify({ num, action, username, password })}` + ); + + const form = document.createElement("form"); + form.id = `form${num}`; + form.action = action; + form.onsubmit = () => false; + + if (autocomplete != null) { + form.setAttribute("autocomplete", autocomplete); + } + + const usernameInput = document.createElement("input"); + + usernameInput.type = username.type; + usernameInput.name = username.name; + + if (username.id != null) { + usernameInput.id = username.id; + } + if (username.value != null) { + usernameInput.value = username.value; + } + if (username.autocomplete != null) { + usernameInput.setAttribute("autocomplete", username.autocomplete); + } + + form.appendChild(usernameInput); + + if (password) { + const passwordInput = document.createElement("input"); + + passwordInput.type = password.type; + passwordInput.name = password.name; + + if (password.id != null) { + passwordInput.id = password.id; + } + if (password.value != null) { + passwordInput.value = password.value; + } + if (password.autocomplete != null) { + passwordInput.setAttribute("autocomplete", password.autocomplete); + } + if (password.readonly != null) { + passwordInput.setAttribute("readonly", password.readonly); + } + if (password.disabled != null) { + passwordInput.setAttribute("disabled", password.disabled); + } + + if (password.label != null) { + const passwordLabel = document.createElement("label"); + passwordLabel.innerText = password.label; + passwordLabel.appendChild(passwordInput); + form.appendChild(passwordLabel); + } else { + form.appendChild(passwordInput); + } + } + + const submitButton = document.createElement("button"); + submitButton.type = "submit"; + submitButton.name = "submit"; + submitButton.innerText = "Submit"; + form.appendChild(submitButton); + + const content = document.getElementById("content"); + + const oldForm = document.getElementById(form.id); + if (oldForm) { + content.replaceChild(form, oldForm); + } else { + content.appendChild(form); + } + + return form; +} + +/** + * Check for expected username/password in form. + * @see `checkForm` below for a similar function. + */ +function checkLoginForm( + usernameField, + expectedUsername, + passwordField, + expectedPassword +) { + let formID = usernameField.parentNode.id; + is( + usernameField.value, + expectedUsername, + "Checking " + formID + " username is: " + expectedUsername + ); + is( + passwordField.value, + expectedPassword, + "Checking " + formID + " password is: " + expectedPassword + ); +} + +/** + * Check repeatedly for a while to see if a particular condition still applies. + * This function checks the return value of `condition` repeatedly until either + * the condition has a falsy return value, or `retryTimes` is exceeded. + */ + +function ensureCondition( + condition, + errorMsg = "Condition did not last.", + retryTimes = 10 +) { + return new Promise((resolve, reject) => { + let tries = 0; + let conditionFailed = false; + let interval = setInterval(async function () { + try { + const conditionPassed = await condition(); + conditionFailed ||= !conditionPassed; + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionFailed = true; + } + if (conditionFailed || tries >= retryTimes) { + ok(!conditionFailed, errorMsg); + clearInterval(interval); + if (conditionFailed) { + reject(errorMsg); + } else { + resolve(); + } + } + tries++; + }, 100); + }); +} + +/** + * Wait a while to ensure login form stays filled with username and password + * @see `checkLoginForm` below for a similar function. + * @returns a promise, resolving when done + * + * TODO: eventually get rid of this time based check, and transition to an + * event based approach. See Bug 1811142. + * Filling happens by `_fillForm()` which can report it's decision and we can + * wait for it. One of the options is to have `didFillFormAsync()` from + * https://phabricator.services.mozilla.com/D167214#change-3njWgUgqswws + */ +function ensureLoginFormStaysFilledWith( + usernameField, + expectedUsername, + passwordField, + expectedPassword +) { + return ensureCondition(() => { + return ( + Object.is(usernameField.value, expectedUsername) && + Object.is(passwordField.value, expectedPassword) + ); + }, `Ensuring form ${usernameField.parentNode.id} stays filled with "${expectedUsername}:${expectedPassword}"`); +} + +function checkLoginFormInFrame( + iframeBC, + usernameFieldId, + expectedUsername, + passwordFieldId, + expectedPassword +) { + return SpecialPowers.spawn( + iframeBC, + [usernameFieldId, expectedUsername, passwordFieldId, expectedPassword], + ( + usernameFieldIdF, + expectedUsernameF, + passwordFieldIdF, + expectedPasswordF + ) => { + let usernameField = + this.content.document.getElementById(usernameFieldIdF); + let passwordField = + this.content.document.getElementById(passwordFieldIdF); + + let formID = usernameField.parentNode.id; + Assert.equal( + usernameField.value, + expectedUsernameF, + "Checking " + formID + " username is: " + expectedUsernameF + ); + Assert.equal( + passwordField.value, + expectedPasswordF, + "Checking " + formID + " password is: " + expectedPasswordF + ); + } + ); +} + +async function checkUnmodifiedFormInFrame(bc, formNum) { + return SpecialPowers.spawn(bc, [formNum], formNumF => { + let form = this.content.document.getElementById(`form${formNumF}`); + ok(form, "Locating form " + formNumF); + + for (var i = 0; i < form.elements.length; i++) { + var ele = form.elements[i]; + + // No point in checking form submit/reset buttons. + if (ele.type == "submit" || ele.type == "reset") { + continue; + } + + is( + ele.value, + ele.defaultValue, + "Test to default value of field " + ele.name + " in form " + formNumF + ); + } + }); +} + +/** + * Check a form for expected values even if it is in a different top level window + * or process. If an argument is null, a field's expected value will be the default + * value. + * + * Similar to the checkForm helper, but it works across (cross-origin) frames. + * + * <form id="form#"> + * checkLoginFormInFrameWithElementValues(#, "foo"); + */ +async function checkLoginFormInFrameWithElementValues( + browsingContext, + formNum, + ...values +) { + return SpecialPowers.spawn( + browsingContext, + [formNum, values], + function checkFormWithElementValues(formNumF, valuesF) { + let [val1F, val2F, val3F] = valuesF; + let doc = this.content.document; + let e; + let form = doc.getElementById("form" + formNumF); + ok(form, "Locating form " + formNumF); + + let numToCheck = arguments.length - 1; + + if (!numToCheck--) { + return; + } + e = form.elements[0]; + if (val1F == null) { + is( + e.value, + e.defaultValue, + "Test default value of field " + e.name + " in form " + formNumF + ); + } else { + is( + e.value, + val1F, + "Test value of field " + e.name + " in form " + formNumF + ); + } + + if (!numToCheck--) { + return; + } + + e = form.elements[1]; + if (val2F == null) { + is( + e.value, + e.defaultValue, + "Test default value of field " + e.name + " in form " + formNumF + ); + } else { + is( + e.value, + val2F, + "Test value of field " + e.name + " in form " + formNumF + ); + } + + if (!numToCheck--) { + return; + } + e = form.elements[2]; + if (val3F == null) { + is( + e.value, + e.defaultValue, + "Test default value of field " + e.name + " in form " + formNumF + ); + } else { + is( + e.value, + val3F, + "Test value of field " + e.name + " in form " + formNumF + ); + } + } + ); +} + +/** + * Check a form for expected values. If an argument is null, a field's + * expected value will be the default value. + * + * <form id="form#"> + * checkForm(#, "foo"); + */ +function checkForm(formNum, val1, val2, val3) { + var e, + form = document.getElementById("form" + formNum); + ok(form, "Locating form " + formNum); + + var numToCheck = arguments.length - 1; + + if (!numToCheck--) { + return; + } + e = form.elements[0]; + if (val1 == null) { + is( + e.value, + e.defaultValue, + "Test default value of field " + e.name + " in form " + formNum + ); + } else { + is(e.value, val1, "Test value of field " + e.name + " in form " + formNum); + } + + if (!numToCheck--) { + return; + } + e = form.elements[1]; + if (val2 == null) { + is( + e.value, + e.defaultValue, + "Test default value of field " + e.name + " in form " + formNum + ); + } else { + is(e.value, val2, "Test value of field " + e.name + " in form " + formNum); + } + + if (!numToCheck--) { + return; + } + e = form.elements[2]; + if (val3 == null) { + is( + e.value, + e.defaultValue, + "Test default value of field " + e.name + " in form " + formNum + ); + } else { + is(e.value, val3, "Test value of field " + e.name + " in form " + formNum); + } +} + +/** + * Check a form for unmodified values from when page was loaded. + * + * <form id="form#"> + * checkUnmodifiedForm(#); + */ +function checkUnmodifiedForm(formNum) { + var form = document.getElementById("form" + formNum); + ok(form, "Locating form " + formNum); + + for (var i = 0; i < form.elements.length; i++) { + var ele = form.elements[i]; + + // No point in checking form submit/reset buttons. + if (ele.type == "submit" || ele.type == "reset") { + continue; + } + + is( + ele.value, + ele.defaultValue, + "Test to default value of field " + ele.name + " in form " + formNum + ); + } +} + +/** + * Wait for the document to be ready and any existing password fields on + * forms to be processed. + * + * @param existingPasswordFieldsCount the number of password fields + * that begin on the test page. + */ +function registerRunTests(existingPasswordFieldsCount = 0, callback) { + return new Promise(resolve => { + function onDOMContentLoaded() { + var form = document.createElement("form"); + form.id = "observerforcer"; + var username = document.createElement("input"); + username.name = "testuser"; + form.appendChild(username); + var password = document.createElement("input"); + password.name = "testpass"; + password.type = "password"; + form.appendChild(password); + + let foundForcer = false; + var observer = SpecialPowers.wrapCallback(function ( + subject, + topic, + data + ) { + if (data === "observerforcer") { + foundForcer = true; + } else { + existingPasswordFieldsCount--; + } + + if (!foundForcer || existingPasswordFieldsCount > 0) { + return; + } + + SpecialPowers.removeObserver(observer, "passwordmgr-processed-form"); + form.remove(); + SimpleTest.executeSoon(() => { + callback?.(); + resolve(); + }); + }); + SpecialPowers.addObserver(observer, "passwordmgr-processed-form"); + + document.body.appendChild(form); + } + // We provide a general mechanism for our tests to know when they can + // safely run: we add a final form that we know will be filled in, wait + // for the login manager to tell us that it's filled in and then continue + // with the rest of the tests. + if ( + document.readyState == "complete" || + document.readyState == "interactive" + ) { + onDOMContentLoaded(); + } else { + window.addEventListener("DOMContentLoaded", onDOMContentLoaded); + } + }); +} + +function enablePrimaryPassword() { + setPrimaryPassword(true); +} + +function disablePrimaryPassword() { + setPrimaryPassword(false); +} + +function setPrimaryPassword(enable) { + PWMGR_COMMON_PARENT.sendAsyncMessage("setPrimaryPassword", { enable }); +} + +function isLoggedIn() { + return PWMGR_COMMON_PARENT.sendQuery("isLoggedIn"); +} + +function logoutPrimaryPassword() { + runInParent(function parent_logoutPrimaryPassword() { + var sdr = Cc["@mozilla.org/security/sdr;1"].getService( + Ci.nsISecretDecoderRing + ); + sdr.logoutAndTeardown(); + }); +} + +/** + * Resolves when a specified number of forms have been processed for (potential) filling. + * This relies on the observer service which only notifies observers within the same process. + */ +function promiseFormsProcessedInSameProcess(expectedCount = 1) { + var processedCount = 0; + return new Promise((resolve, reject) => { + function onProcessedForm(subject, topic, data) { + processedCount++; + if (processedCount == expectedCount) { + info(`${processedCount} form(s) processed`); + SpecialPowers.removeObserver( + onProcessedForm, + "passwordmgr-processed-form" + ); + resolve(SpecialPowers.Cu.waiveXrays(subject), data); + } + } + SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form"); + }); +} + +/** + * Resolves when a form has been processed for (potential) filling. + * This works across processes. + */ +async function promiseFormsProcessed(expectedCount = 1) { + info(`waiting for ${expectedCount} forms to be processed`); + var processedCount = 0; + return new Promise(resolve => { + PWMGR_COMMON_PARENT.addMessageListener( + "formProcessed", + function formProcessed() { + processedCount++; + info(`processed form ${processedCount} of ${expectedCount}`); + if (processedCount == expectedCount) { + info(`processing of ${expectedCount} forms complete`); + PWMGR_COMMON_PARENT.removeMessageListener( + "formProcessed", + formProcessed + ); + resolve(); + } + } + ); + }); +} + +async function loadFormIntoWindow(origin, html, win, expectedCount = 1, task) { + let loadedPromise = new Promise(resolve => { + win.addEventListener( + "load", + function (event) { + if (event.target.location.href.endsWith("blank.html")) { + resolve(); + } + }, + { once: true } + ); + }); + + let processedPromise = promiseFormsProcessed(expectedCount); + win.location = + origin + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"; + info(`Waiting for window to load for origin: ${origin}`); + await loadedPromise; + + await SpecialPowers.spawn( + win, + [html, task?.toString()], + function (contentHtml, contentTask = null) { + this.content.document.documentElement.innerHTML = contentHtml; + // Similar to the invokeContentTask helper in accessible/tests/browser/shared-head.js + if (contentTask) { + // eslint-disable-next-line no-eval + const runnableTask = eval(` + (() => { + return (${contentTask}); + })();`); + runnableTask.call(this); + } + } + ); + + info("Waiting for the form to be processed"); + await processedPromise; +} + +async function getTelemetryEvents(options) { + let events = await PWMGR_COMMON_PARENT.sendQuery( + "getTelemetryEvents", + options + ); + info("CONTENT: getTelemetryEvents gotResult: " + JSON.stringify(events)); + return events; +} + +function loadRecipes(recipes) { + info("Loading recipes"); + return PWMGR_COMMON_PARENT.sendQuery("loadRecipes", recipes); +} + +function resetRecipes() { + info("Resetting recipes"); + return PWMGR_COMMON_PARENT.sendQuery("resetRecipes"); +} + +async function promiseStorageChanged(expectedChangeTypes) { + let result = await PWMGR_COMMON_PARENT.sendQuery("storageChanged", { + expectedChangeTypes, + }); + + if (result) { + ok(false, result); + } +} + +async function promisePromptShown(expectedTopic) { + let topic = await PWMGR_COMMON_PARENT.sendQuery("promptShown"); + is(topic, expectedTopic, "Check expected prompt topic"); +} + +/** + * Run a function synchronously in the parent process and destroy it in the test cleanup function. + * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run + * or the URL to a JS file. + * @return {Object} - the return value of loadChromeScript providing message-related methods. + * @see loadChromeScript in specialpowersAPI.js + */ +function runInParent(aFunctionOrURL) { + let chromeScript = SpecialPowers.loadChromeScript(aFunctionOrURL); + SimpleTest.registerCleanupFunction(() => { + chromeScript.destroy(); + }); + return chromeScript; +} + +/** Manage logins in parent chrome process. + * */ +function manageLoginsInParent() { + return runInParent(function addLoginsInParentInner() { + /* eslint-env mozilla/chrome-script */ + addMessageListener("removeAllUserFacingLogins", () => { + Services.logins.removeAllUserFacingLogins(); + }); + + /* eslint-env mozilla/chrome-script */ + addMessageListener("getLogins", async () => { + const logins = await Services.logins.getAllLogins(); + return logins.map( + ({ + origin, + formActionOrigin, + httpRealm, + username, + password, + usernameField, + passwordField, + }) => [ + origin, + formActionOrigin, + httpRealm, + username, + password, + usernameField, + passwordField, + ] + ); + }); + + /* eslint-env mozilla/chrome-script */ + addMessageListener("addLogins", async logins => { + let nsLoginInfo = Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, + "init" + ); + + const loginInfos = logins.map(login => new nsLoginInfo(...login)); + try { + await Services.logins.addLogins(loginInfos); + } catch (e) { + assert.ok(false, "addLogins threw: " + e); + } + }); + }); +} + +/** Initialize with a list of logins. The logins are added within the parent chrome process. + * @param {array} aLogins - a list of logins to add. Each login is an array of the arguments + * that would be passed to nsLoginInfo.init(). + */ +async function addLoginsInParent(...aLogins) { + const script = manageLoginsInParent(); + await script.sendQuery("addLogins", aLogins); + return script; +} + +/** Initialize with a list of logins, after removing all user facing logins. + * The logins are added within the parent chrome process. + * @param {array} aLogins - a list of logins to add. Each login is an array of the arguments + * that would be passed to nsLoginInfo.init(). + */ +async function setStoredLoginsAsync(...aLogins) { + const script = manageLoginsInParent(); + script.sendQuery("removeAllUserFacingLogins"); + await script.sendQuery("addLogins", aLogins); + return script; +} + +/** + * Sets given logins for the duration of the test. Existing logins are first + * removed and finally restored when the test is finished. + * The logins are added within the parent chrome process. + * @param {array} logins - a list of logins to add. Each login is an array of the arguments + * that would be passed to nsLoginInfo.init(). + */ +async function setStoredLoginsDuringTest(...logins) { + const script = manageLoginsInParent(); + const loginsBefore = await script.sendQuery("getLogins"); + await script.sendQuery("removeAllUserFacingLogins"); + await script.sendQuery("addLogins", logins); + SimpleTest.registerCleanupFunction(async () => { + await script.sendQuery("removeAllUserFacingLogins"); + await script.sendQuery("addLogins", loginsBefore); + }); +} + +/** + * Sets given logins for the duration of the task. Existing logins are first + * removed and finally restored when the task is finished. + * @param {array} logins - a list of logins to add. Each login is an array of the arguments + * that would be passed to nsLoginInfo.init(). + */ +async function setStoredLoginsDuringTask(...logins) { + const script = manageLoginsInParent(); + const loginsBefore = await script.sendQuery("getLogins"); + await script.sendQuery("removeAllUserFacingLogins"); + await script.sendQuery("addLogins", logins); + SimpleTest.registerTaskCleanupFunction(async () => { + await script.sendQuery("removeAllUserFacingLogins"); + await script.sendQuery("addLogins", loginsBefore); + }); +} + +/** Returns a promise which resolves to a list of logins + */ +function getLogins() { + const script = manageLoginsInParent(); + return script.sendQuery("getLogins"); +} + +/* + * gTestDependsOnDeprecatedLogin Set this global to true if your test relies + * on the testuser/testpass login that is created in pwmgr_common.js. New tests + * should not rely on this login. + */ +var gTestDependsOnDeprecatedLogin = false; + +/** + * Replace the content innerHTML with the provided form and wait for autofill to fill in the form. + * + * @param {string} form The form to be appended to the #content element. + * @param {string} fieldSelector The CSS selector for the field to-be-filled + * @param {string} fieldValue The value expected to be filled + * @param {string} formId The ID (excluding the # character) of the form + */ +function setFormAndWaitForFieldFilled( + form, + { fieldSelector, fieldValue, formId } +) { + document.querySelector("#content").innerHTML = form; + return SimpleTest.promiseWaitForCondition(() => { + let ancestor = formId + ? document.querySelector("#" + formId) + : document.documentElement; + return ancestor.querySelector(fieldSelector).value == fieldValue; + }, "Wait for password manager to fill form"); +} + +/** + * Run commonInit synchronously in the parent then run the test function if supplied. + * + * @param {Function} aFunction The test function to run + */ +async function runChecksAfterCommonInit(aFunction = null) { + SimpleTest.waitForExplicitFinish(); + await PWMGR_COMMON_PARENT.sendQuery("setupParent", { + testDependsOnDeprecatedLogin: gTestDependsOnDeprecatedLogin, + }); + + if (aFunction) { + await registerRunTests(0, aFunction); + } + + return PWMGR_COMMON_PARENT; +} + +// Begin code that runs immediately for all tests that include this file. + +const PWMGR_COMMON_PARENT = runInParent( + SimpleTest.getTestFileURL("pwmgr_common_parent.js") +); + +SimpleTest.registerCleanupFunction(() => { + SpecialPowers.flushPrefEnv(); + + PWMGR_COMMON_PARENT.sendAsyncMessage("cleanup"); + + runInParent(function cleanupParent() { + /* eslint-env mozilla/chrome-script */ + // eslint-disable-next-line no-shadow + const { LoginManagerParent } = ChromeUtils.importESModule( + "resource://gre/modules/LoginManagerParent.sys.mjs" + ); + + // Remove all logins and disabled hosts + Services.logins.removeAllUserFacingLogins(); + + let disabledHosts = Services.logins.getAllDisabledHosts(); + disabledHosts.forEach(host => + Services.logins.setLoginSavingEnabled(host, true) + ); + + let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService( + Ci.nsIHttpAuthManager + ); + authMgr.clearAll(); + + // Check that it's not null, instead of truthy to catch it becoming undefined + // in a refactoring. + if (LoginManagerParent._recipeManager !== null) { + LoginManagerParent._recipeManager.reset(); + } + + // Cleanup PopupNotifications (if on a relevant platform) + let chromeWin = Services.wm.getMostRecentWindow("navigator:browser"); + if (chromeWin && chromeWin.PopupNotifications) { + let notes = chromeWin.PopupNotifications._currentNotifications; + if (notes.length) { + dump("Removing " + notes.length + " popup notifications.\n"); + } + for (let note of notes) { + note.remove(); + } + } + + // Clear events last in case the above cleanup records events. + Services.telemetry.clearEvents(); + }); +}); + +/** + * Proxy for Services.logins (nsILoginManager). + * Only supports arguments which support structured clone plus {nsILoginInfo} + * Assumes properties are methods. + */ +this.LoginManager = new Proxy( + {}, + { + get(target, prop, receiver) { + return (...args) => { + let loginInfoIndices = []; + let cloneableArgs = args.map((val, index) => { + if ( + SpecialPowers.call_Instanceof(val, SpecialPowers.Ci.nsILoginInfo) + ) { + loginInfoIndices.push(index); + return LoginHelper.loginToVanillaObject(val); + } + + return val; + }); + + return PWMGR_COMMON_PARENT.sendQuery("proxyLoginManager", { + args: cloneableArgs, + loginInfoIndices, + methodName: prop, + }); + }; + }, + } +); + +/** + * Set the inner html of the content div and ensure it gets reset after current + * task finishes. + * Returns the first child node of the newly created content div for convenient + * access of the newly created dom node. + * + * @param {String} html + * string of dom content or dom element to be inserted into content element + */ +function setContentForTask(html) { + const content = document.querySelector("#content"); + const innerHTMLBefore = content.innerHTML || ""; + SimpleTest.registerCurrentTaskCleanupFunction( + () => (content.innerHTML = innerHTMLBefore) + ); + if (html.content?.cloneNode) { + const clone = html.content.cloneNode(true); + content.replaceChildren(clone); + } else { + content.innerHTML = html; + } + return content.firstElementChild; +} + +/* + * Set preferences via SpecialPowers.pushPrefEnv and reset them after current + * task has finished. + * + * @param {*Object} preferences + * */ +async function setPreferencesForTask(...preferences) { + await SpecialPowers.pushPrefEnv({ + set: preferences, + }); + SimpleTest.registerCurrentTaskCleanupFunction(() => SpecialPowers.popPrefEnv); +} + +// capture form autofill results between tasks +let gPwmgrCommonCapturedAutofillResults = {}; +PWMGR_COMMON_PARENT.addMessageListener( + "formProcessed", + ({ formId, autofillResult }) => { + if (formId === "observerforcer") { + return; + } + + gPwmgrCommonCapturedAutofillResults[formId] = autofillResult; + } +); +SimpleTest.registerTaskCleanupFunction(() => { + gPwmgrCommonCapturedAutofillResults = {}; +}); + +/** + * Create a promise that resolves when the form has been processed. + * Works with forms processed in the past since the task started and in the future, + * across parent and child processes. + * + * @param {String} formId / the id of the form of which to expect formautofill events + * @returns promise, resolving with the autofill result. + */ +async function formAutofillResult(formId) { + if (formId in gPwmgrCommonCapturedAutofillResults) { + const autofillResult = gPwmgrCommonCapturedAutofillResults[formId]; + delete gPwmgrCommonCapturedAutofillResults[formId]; + return autofillResult; + } + return new Promise((resolve, reject) => { + PWMGR_COMMON_PARENT.addMessageListener( + "formProcessed", + ({ formId: id, autofillResult }) => { + if (id !== formId) { + return; + } + delete gPwmgrCommonCapturedAutofillResults[formId]; + resolve(autofillResult); + }, + { once: true } + ); + }); +} diff --git a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js new file mode 100644 index 0000000000..09ad80f467 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js @@ -0,0 +1,247 @@ +/** + * Loaded as a frame script to do privileged things in mochitest-plain tests. + * See pwmgr_common.js for the content process companion. + */ + +/* eslint-env mozilla/chrome-script */ + +"use strict"; + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { LoginHelper } = ChromeUtils.importESModule( + "resource://gre/modules/LoginHelper.sys.mjs" +); +var { LoginManagerParent } = ChromeUtils.importESModule( + "resource://gre/modules/LoginManagerParent.sys.mjs" +); +const { LoginTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" +); +if (LoginHelper.relatedRealmsEnabled) { + let rsPromise = + LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials(); + async () => { + await rsPromise; + }; +} +if (LoginHelper.improvedPasswordRulesEnabled) { + let rsPromise = LoginTestUtils.remoteSettings.setupImprovedPasswordRules({ + rules: "", + }); + async () => { + await rsPromise; + }; +} + +/** + * Init with a common login. + */ +async function commonInit(testDependsOnDeprecatedLogin) { + var pwmgr = Services.logins; + assert.ok(pwmgr != null, "Access LoginManager"); + + // Check that initial state has no logins + var logins = await pwmgr.getAllLogins(); + assert.equal(logins.length, 0, "Not expecting logins to be present"); + var disabledHosts = pwmgr.getAllDisabledHosts(); + if (disabledHosts.length) { + assert.ok(false, "Warning: wasn't expecting disabled hosts to be present."); + for (var host of disabledHosts) { + pwmgr.setLoginSavingEnabled(host, true); + } + } + + if (testDependsOnDeprecatedLogin) { + // Add a login that's used in multiple tests + var login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + login.init( + "http://mochi.test:8888", + "http://mochi.test:8888", + null, + "testuser", + "testpass", + "uname", + "pword" + ); + await pwmgr.addLoginAsync(login); + } + + // Last sanity check + logins = await pwmgr.getAllLogins(); + assert.equal( + logins.length, + testDependsOnDeprecatedLogin ? 1 : 0, + "Checking for successful init login" + ); + disabledHosts = pwmgr.getAllDisabledHosts(); + assert.equal(disabledHosts.length, 0, "Checking for no disabled hosts"); +} + +async function dumpLogins() { + let logins = await Services.logins.getAllLogins(); + assert.ok(true, "----- dumpLogins: have " + logins.length + " logins. -----"); + for (var i = 0; i < logins.length; i++) { + dumpLogin("login #" + i + " --- ", logins[i]); + } +} + +function dumpLogin(label, login) { + var loginText = ""; + loginText += "origin: "; + loginText += login.origin; + loginText += " / formActionOrigin: "; + loginText += login.formActionOrigin; + loginText += " / realm: "; + loginText += login.httpRealm; + loginText += " / user: "; + loginText += login.username; + loginText += " / pass: "; + loginText += login.password; + loginText += " / ufield: "; + loginText += login.usernameField; + loginText += " / pfield: "; + loginText += login.passwordField; + assert.ok(true, label + loginText); +} + +addMessageListener("storageChanged", async function ({ expectedChangeTypes }) { + return new Promise((resolve, reject) => { + function storageChanged(subject, topic, data) { + let changeType = expectedChangeTypes.shift(); + if (data != changeType) { + resolve("Unexpected change type " + data + ", expected " + changeType); + } else if (expectedChangeTypes.length === 0) { + Services.obs.removeObserver( + storageChanged, + "passwordmgr-storage-changed" + ); + resolve(); + } + } + + Services.obs.addObserver(storageChanged, "passwordmgr-storage-changed"); + }); +}); + +addMessageListener("promptShown", async function () { + return new Promise(resolve => { + function promptShown(subject, topic, data) { + Services.obs.removeObserver(promptShown, "passwordmgr-prompt-change"); + Services.obs.removeObserver(promptShown, "passwordmgr-prompt-save"); + resolve(topic); + } + + Services.obs.addObserver(promptShown, "passwordmgr-prompt-change"); + Services.obs.addObserver(promptShown, "passwordmgr-prompt-save"); + }); +}); + +addMessageListener("cleanup", () => { + Services.logins.removeAllUserFacingLogins(); +}); + +// Begin message listeners + +addMessageListener( + "setupParent", + async ({ testDependsOnDeprecatedLogin = false } = {}) => { + return commonInit(testDependsOnDeprecatedLogin); + } +); + +addMessageListener("loadRecipes", async function (recipes) { + var recipeParent = await LoginManagerParent.recipeParentPromise; + await recipeParent.load(recipes); + return recipes; +}); + +addMessageListener("resetRecipes", async function () { + let recipeParent = await LoginManagerParent.recipeParentPromise; + await recipeParent.reset(); +}); + +addMessageListener("getTelemetryEvents", options => { + options = Object.assign( + { + filterProps: {}, + clear: false, + }, + options + ); + let snapshots = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + options.clear + ); + let events = options.process in snapshots ? snapshots[options.process] : []; + + // event is array of values like: [22476,"pwmgr","autocomplete_field","generatedpassword"] + let keys = ["id", "category", "method", "object", "value"]; + events = events.filter(entry => { + for (let idx = 0; idx < keys.length; idx++) { + let key = keys[idx]; + if ( + key in options.filterProps && + options.filterProps[key] !== entry[idx] + ) { + return false; + } + } + return true; + }); + + return events; +}); + +addMessageListener("proxyLoginManager", async msg => { + // Recreate nsILoginInfo objects from vanilla JS objects. + let recreatedArgs = msg.args.map((arg, index) => { + if (msg.loginInfoIndices.includes(index)) { + return LoginHelper.vanillaObjectToLogin(arg); + } + + return arg; + }); + + let rv = await Services.logins[msg.methodName](...recreatedArgs); + if (rv instanceof Ci.nsILoginInfo) { + rv = LoginHelper.loginToVanillaObject(rv); + } else if ( + Array.isArray(rv) && + !!rv.length && + rv[0] instanceof Ci.nsILoginInfo + ) { + rv = rv.map(login => LoginHelper.loginToVanillaObject(login)); + } + return rv; +}); + +addMessageListener("isLoggedIn", () => { + // This can't use the LoginManager proxy in pwmgr_common.js since it's not a method. + return Services.logins.isLoggedIn; +}); + +addMessageListener("setPrimaryPassword", ({ enable }) => { + if (enable) { + LoginTestUtils.primaryPassword.enable(); + } else { + LoginTestUtils.primaryPassword.disable(); + } +}); + +LoginManagerParent.setListenerForTests((msg, { origin, data }) => { + if (msg == "ShowDoorhanger") { + sendAsyncMessage("formSubmissionProcessed", { origin, data }); + } else if (msg == "PasswordEditedOrGenerated") { + sendAsyncMessage("passwordEditedOrGenerated", { origin, data }); + } else if (msg == "FormProcessed") { + sendAsyncMessage("formProcessed", data); + } +}); + +addMessageListener("cleanup", () => { + LoginManagerParent.setListenerForTests(null); +}); diff --git a/toolkit/components/passwordmgr/test/mochitest/slow_image.html b/toolkit/components/passwordmgr/test/mochitest/slow_image.html new file mode 100644 index 0000000000..172d592633 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/slow_image.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + <img src="slow_image.sjs" /> + </body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/slow_image.sjs b/toolkit/components/passwordmgr/test/mochitest/slow_image.sjs new file mode 100644 index 0000000000..b955e43f5d --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/slow_image.sjs @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// The delay time will not impact test running time, as the test does +// not wait for the "load" event. We just need to ensure the pwmgr code +// e.g. autofill happens before the delay time elapses. +const DELAY_MS = "5000"; + +let timer; + +function handleRequest(req, resp) { + resp.processAsync(); + resp.setHeader("Cache-Control", "no-cache", false); + resp.setHeader("Content-Type", "image/png", false); + + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + () => { + resp.write(""); + resp.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/toolkit/components/passwordmgr/test/mochitest/subtst_prefilled_form.html b/toolkit/components/passwordmgr/test/mochitest/subtst_prefilled_form.html new file mode 100644 index 0000000000..8a3e0b0240 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/subtst_prefilled_form.html @@ -0,0 +1,18 @@ +<!DOCTYPE html><html><head><meta charset="utf-8"></head><body> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<!-- Form with username and password fields pre-populated. + With a couple elements where .defaultValue !== .value --> +<form id="form-basic"> + <input id="form-basic-username" name="username" value="user"> + <input id="form-basic-password" name="password" type="password" value="pass"> + <select name="picker"> + <option value="foo">0</option> + <option value="bar" selected>1</option> + </select> + <input id="form-basic-submit" type="submit"> + <button id="form-basic-reset" type="reset">Reset</button> +</form> + +</body></html> diff --git a/toolkit/components/passwordmgr/test/mochitest/subtst_primary_pass.html b/toolkit/components/passwordmgr/test/mochitest/subtst_primary_pass.html new file mode 100644 index 0000000000..dc892c50e2 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/subtst_primary_pass.html @@ -0,0 +1,8 @@ +<h2>MP subtest</h2> +This form triggers a MP and gets filled in.<br> +<form> +Username: <input type="text" id="userfield" name="u"><br> +Password: <input type="password" id="passfield" name="p" + oninput="parent.postMessage('filled', '*');"><br> +</form> +<iframe></iframe> diff --git a/toolkit/components/passwordmgr/test/mochitest/subtst_prompt_async.html b/toolkit/components/passwordmgr/test/mochitest/subtst_prompt_async.html new file mode 100644 index 0000000000..39a72add56 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/subtst_prompt_async.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Multiple auth request</title> +</head> +<body> + <iframe id="iframe1" src="authenticate.sjs?r=1&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe> + <iframe id="iframe2" src="authenticate.sjs?r=2&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe> + <iframe id="iframe3" src="authenticate.sjs?r=3&user=user3name&pass=user3pass&realm=mochirealm3&proxy_user=proxy_user2&proxy_pass=proxy_pass2&proxy_realm=proxy_realm2"></iframe> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html b/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html new file mode 100644 index 0000000000..3ab40e92af --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test the password manager code called on DOMInputPasswordAdded runs when it occurs between DOMContentLoaded and load events</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <!-- In cases where the "DOMContentLoaded" event for a page has occured but not the "load" event when + "DOMInputPasswordAdded" fires, we want to make sure that the Password Manager code (i.e. + _fetchLoginsFromParentAndFillForm) still runs on the page. + This scenario can happen for example when a page has very little initial HTML, but extensive JS that + adds Custom Elements or other HTML later, or when other subresources, like images, take a while to load. + In this test, we delay the page load with a delayed response for an image source. --> +<script type="application/javascript"> + +let DEFAULT_ORIGIN = window.location.origin; +let FILE_PATH = "/tests/toolkit/components/passwordmgr/test/mochitest/slow_image.html"; + +async function openDocumentInWindow(win) { + let DOMContentLoadedPromise = new Promise((resolve) => { + win.addEventListener("DOMContentLoaded", function() { + resolve(); + }, {once: true}); + }); + win.location = DEFAULT_ORIGIN + FILE_PATH; + await DOMContentLoadedPromise; +} + +add_setup(async () => { + await setStoredLoginsAsync([DEFAULT_ORIGIN, DEFAULT_ORIGIN, null, "user", "omgsecret!"]); +}); + +add_task(async function test_password_autofilled() { + let numLogins = await LoginManager.countLogins(DEFAULT_ORIGIN, DEFAULT_ORIGIN, null); + is(numLogins, 1, "Correct number of logins"); + + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await openDocumentInWindow(win); + let processedPromise = promiseFormsProcessed(); + await SpecialPowers.spawn(win, [], function() { + let doc = this.content.document; + info("Adding password input field to the page to trigger DOMInputPasswordAdded"); + let passwordField = doc.createElement("input"); + passwordField.type = "password"; + is(doc.readyState, "interactive", "Make sure 'DOMContentLoaded' has fired but not 'load'"); + doc.body.append(passwordField); + }); + info("Waiting for the password field to be autofilled"); + await processedPromise; + let expectedValue = "omgsecret!"; + await SpecialPowers.spawn(win, [expectedValue], expectedValueF => { + is(this.content.document.querySelector("input[type='password']").value, expectedValueF, "Ensure the password field is autofilled"); + }); +}); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_LoginManagerContent_passwordEditedOrGenerated.html b/toolkit/components/passwordmgr/test/mochitest/test_LoginManagerContent_passwordEditedOrGenerated.html new file mode 100644 index 0000000000..9d23177924 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_LoginManagerContent_passwordEditedOrGenerated.html @@ -0,0 +1,160 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test behavior of unmasking in LMC._passwordEditedOrGenerated</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script> +const { LoginManagerChild } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/LoginManagerChild.sys.mjs" +); + +function preventDefaultAndStopProgagation(event) { + event.preventDefault(); + event.stopImmediatePropagation(); +} + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ]}); + await setStoredLoginsAsync( + [location.origin, "https://autofill", null, "user1", "pass1"], + [location.origin, "https://autofill", null, "user2", "pass2"] + ); +}); + +add_task(async function prevent_default_and_stop_propagation() { + const form = createLoginForm({ + action: "https://autofill" + }); + await promiseFormsProcessedInSameProcess(); + form.pword.focus(); + + form.pword.addEventListener("focus", preventDefaultAndStopProgagation); + form.pword.addEventListener("focus", preventDefaultAndStopProgagation, true); + form.pword.addEventListener("blur", preventDefaultAndStopProgagation); + form.pword.addEventListener("blur", preventDefaultAndStopProgagation, true); + + SpecialPowers.wrap(form.pword).setUserInput("generatedpass"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill"); + LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true }); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After shift-tab to focus again"); +}); + +add_task(async function fields_masked_after_saved_login_fill() { + const form = createLoginForm({ + action: "https://autofill" + }); + await promiseFormsProcessedInSameProcess(); + form.pword.focus(); + + SpecialPowers.wrap(form.pword).setUserInput("generatedpass"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill"); + LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true }); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur"); + + info("Filling username matching saved login"); + sendString("user1"); + + let processedPromise = promiseFormsProcessedInSameProcess(); + synthesizeKey("KEY_Tab"); // focus again and trigger a fill of the matching password + await processedPromise; + is(form.pword.value, "pass1", "Saved password was filled") + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After filling a saved login"); +}); + +add_task(async function fields_masked_after_replacing_whole_value() { + const form = createLoginForm({ + action: "https://autofill" + }); + await promiseFormsProcessedInSameProcess(); + form.pword.focus(); + + SpecialPowers.wrap(form.pword).setUserInput("generatedpass"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill"); + LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true }); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur"); + + synthesizeKey("KEY_Tab"); // focus again and replace the whole password value + info("Replacing password field value with arbitrary string"); + sendString("some_other_password"); + is(form.pword.value, "some_other_password", "Whole password replaced") + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Replaced password value"); + + synthesizeKey("KEY_Tab"); // blur pw + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus pw again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After focus again"); +}); + +add_task(async function fields_unmasked_after_adding_character() { + const form = createLoginForm({ + action: "https://autofill" + }); + await promiseFormsProcessedInSameProcess(); + form.pword.focus(); + + SpecialPowers.wrap(form.pword).setUserInput("generatedpass"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill"); + LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true }); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur"); + + synthesizeKey("KEY_Tab"); // focus again + synthesizeKey("KEY_ArrowRight"); // Remove the selection + info("Adding a character to the end of the password"); + sendString("@"); + is(form.pword.value, "generatedpass@", "Character was added to the value") + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "Added @"); + + synthesizeKey("KEY_Tab"); // blur pw + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur after @"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus pw again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After focus after @"); +}); + +add_task(async function type_not_password() { + const form = createLoginForm({ + action: "https://autofill" + }); + await promiseFormsProcessedInSameProcess(); + form.pword.focus(); + + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "Before first fill"); + SpecialPowers.wrap(form.pword).setUserInput("generatedpass"); + LoginManagerChild.forWindow(window)._passwordEditedOrGenerated(form.pword, { triggeredByFillingGenerated: true }); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After first fill"); + + // Simulate a website doing their own unmasking and re-masking + form.pword.type = "text"; + await new Promise(resolve => SimpleTest.executeSoon(resolve)); + form.pword.type = "password"; + + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "After blur"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "After shift-tab to focus again"); +}); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html new file mode 100644 index 0000000000..b229993a42 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Login Manager: Related Realms</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Testing related realms + +Related realms is feature to provide login suggestion based on similar domains. + +Out of the scope of this feature is subdomain handling, which is already part +of the base functionality. The intent is to cover the ebay.com/ebay.co.uk and +all other country TLD cases where the sign in page is actually +signin.ebay.com/signin.ebay.co.uk but credentials could have manually been +entered for ebay.com/ebay.co.uk or automatically stored as +signin.ebay.com/sigin.ebay.co.uk + +The related realms feature can be enabled via the preference +signon.relatedRealms.enabled. It is disabled by default. + +This test proves that related logins show up in the autocomplete menu when +the feature is enabled and that the related logins do not count as duplicates +and therefore the form gets filled directly. + +<template id="form1-template"> + <form id="form1" action="https://www.example.com"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const formTemplate = document.getElementById("form1-template"); + + add_setup(async () => { + await setStoredLoginsDuringTest( + ["https://example.com", "https://example.com", null, "example.com-user", "password", "uname", "pword"], + + // Related domain relationship where example.com and other-example.com are in the related domains list + ["https://other-example.com", "https://other-example.com", null, "other-example.com-user", "password", "uname", "pword"], + + // example.com and example.co.uk are related, so sub.example.co.uk is also related + ["https://sub.example.co.uk", "https://sub.example.co.uk", null, "sub.example.co.uk-user", "password", "uname", "pword"], + + // www subdomain on same TLD + ["https://www.example.com", "https://www.example.com", null, "www.example.com-user", "password", "uname", "pword"], + + // other subdomain on same TLD + ["https://sub.example.com", "https://sub.example.com", null, "sub.example.com-user", "password", "uname", "pword"], + ); + }); + + add_named_task("with relatedRealms disabled, related logins do not show up in autocomplete menu", async () => { + await setPreferencesForTask(["signon.relatedRealms.enabled", false]); + + const form = setContentForTask(formTemplate); + + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + + // reset the form and open autocomplete popup by focusing username input + form.reset(); + const results = await popupBy(() => form.uname.focus()); + checkAutoCompleteResults(results, [ + "example.com-user", + "sub.example.com-user", + "www.example.com-user", + ], window.location.host, "all logins are present and in order"); + }); + + add_named_task("with relatedRealms enabled, related logins show up in autocomplete menu", async () => { + await setPreferencesForTask(["signon.relatedRealms.enabled", true]); + + const form = setContentForTask(formTemplate); + + const result = await formAutofillResult(form.id); + is(result, "filled", "form has been filled"); + + // reset the form and open autocomplete popup by focusing username input + form.reset(); + const results = await popupBy(() => form.uname.focus()); + checkAutoCompleteResults(results, [ + "example.com-user", + "other-example.com-user", + "sub.example.co.uk-user", + "sub.example.com-user", + "www.example.com-user", + ], window.location.host, "all logins are present and in order"); + }); + + add_named_task("even with relatedRealms enabled, related logins are not considered duplicates, so form will be filled", async () => { + await setPreferencesForTask(["signon.relatedRealms.enabled", true]); + + const form = setContentForTask(formTemplate); + + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + + is(form.uname.value, "example.com-user", "username from exact tld is filled in"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form.html new file mode 100644 index 0000000000..cfa695e002 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form.html @@ -0,0 +1,935 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Login Manager: test basic login autocomplete</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../../../dom/webauthn/tests/u2futil.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Test for Login Manager: login autocomplete, with secure connection. This tests autocomplete menu items, its navigation, the selection and deletion of entries as well as sending untrusted events. + +This tests the login manager in a secure setting using https. A similar test file exists for using an insecure connection: test_autocomplete_basic_form_insecure.html. + +<template id="form1-template"> + <form id="form1" action="https://autocomplete:8888/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const formTemplate = document.getElementById("form1-template"); + + // Restore the form to the default state. + function restoreForm(form) { + form.uname.value = ""; + form.pword.value = ""; + form.uname.focus(); + } + + add_setup(async () => { + listenForUnexpectedPopupShown(); + }); + + add_named_task("form is initially empty and popup closed", async () => { + const form = setContentForTask(formTemplate); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const popupState = await getPopupState(); + is(popupState.open, false, "Check popup is closed"); + }); + + add_named_task("menuitems, telemetry events, selection and escape", async () => { + await setStoredLoginsDuringTask( + // login 0 has no username, so should be filtered out from the autocomplete list. + [location.origin, "https://autocomplete:8888", null, "", "pass0", "", "pword"], + + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + form.uname.focus(); + const autocompleteItems = await popupByArrowDown(); + + const popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + + const expectedMenuItems = ["user-1", + "user-2", + "user-3"]; + checkAutoCompleteResults(autocompleteItems, expectedMenuItems, + window.location.host, "Check all menuitems are displayed correctly."); + + const acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }); + is(acEvents.length, 1, "One autocomplete event"); + checkACTelemetryEvent(acEvents[0], form.uname, { + "hadPrevious": "0", + "login": expectedMenuItems.length + "", + "loginsFooter": "1" + }); + + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Escape"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("select first entry", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + const popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + synthesizeKey("KEY_ArrowDown"); // first + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "pass-1", "password is set"); + }); + + add_named_task("select second entry", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowDown"); // second + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("wrap around first entry", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowDown"); // second + synthesizeKey("KEY_ArrowDown"); // footer + synthesizeKey("KEY_ArrowDown"); // deselects + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "pass-1", "password is set"); + }); + + add_named_task("wrap around up last entry", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_ArrowUp"); // last (fourth) + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("wrap around up down up up", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); // select first entry + synthesizeKey("KEY_ArrowUp"); // selects nothing! + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_ArrowUp"); // select last entry + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("wrap around up down up last", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowUp"); // deselects + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_ArrowUp"); // last entry + synthesizeKey("KEY_ArrowUp"); // first entry + synthesizeKey("KEY_ArrowUp"); // deselects + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_ArrowUp"); // last entry + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("fill username without autofill right", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + // Set first entry w/o triggering autocomplete + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowRight"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("fill username without autofill left", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + // Set first entry w/o triggering autocomplete + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowLeft"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("page up first", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + // Check first entry (page up) + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowDown"); // second + synthesizeKey("KEY_PageUp"); // first + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "pass-1", "password is set"); + }); + + add_named_task("page down last", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + // Check last entry (page down) + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_PageDown"); // footer + synthesizeKey("KEY_ArrowUp"); // last + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("untrusted event", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("delete", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + + // XXX tried sending character "t" before/during dropdown to test + // filtering, but had no luck. Seemed like the character was getting lost. + // Setting uname.value didn't seem to work either. This works with a human + // driver, so I'm not sure what's up. + + // Delete the first entry (of 3), "user-1" + synthesizeKey("KEY_ArrowDown"); + const numLoginsBeforeDeletion = await LoginManager.countLogins(location.origin, "https://autocomplete:8888", null); + is(numLoginsBeforeDeletion, 3, "Correct number of logins before deleting one"); + + const countChangedPromise = notifyMenuChanged(3); + const deletionPromise = promiseStorageChanged(["removeLogin"]); + // On OS X, shift-backspace and shift-delete work, just delete does not. + // On Win/Linux, shift-backspace does not work, delete and shift-delete do. + synthesizeKey("KEY_Delete", {shiftKey: true}); + await deletionPromise; + + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const numLoginsAfterDeletion = await LoginManager.countLogins(location.origin, "https://autocomplete:8888", null); + is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one"); + await countChangedPromise; + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("delete second", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + + // Delete the second entry (of 3), "user-2" + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Delete", {shiftKey: true}); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const numLoginsAfterDeletion = await LoginManager.countLogins(location.origin, "https://autocomplete:8888", null); + is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one"); + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-3", "username is set"); + is(form.pword.value, "pass-3", "password is set"); + }); + + add_named_task("delete last", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + + /* test 54 */ + // Delete the last entry (of 3), "user-3" + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + const numLoginsBeforeDeletion = await LoginManager.countLogins(location.origin, "https://autocomplete:8888", null); + is(numLoginsBeforeDeletion, 3, "Correct number of logins before deleting one"); + synthesizeKey("KEY_Delete", {shiftKey: true}); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const numLoginsAfterDeletion = await LoginManager.countLogins(location.origin, "https://autocomplete:8888", null); + is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "pass-1", "password is set"); + }); + + // Tests for single-user forms for ignoring autocomplete=off */ + + add_named_task("default", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete2"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("password autocomplete off", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete2"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="off"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + + restoreForm(form); + await popupByArrowDown(); + + // Check first entry + synthesizeKey("KEY_ArrowDown"); + // value should not update just on selection + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("username autocomplete off", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete2"> + <input type="text" name="uname" autocomplete="off"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + + restoreForm(form); + await popupByArrowDown(); + + // Check first entry + synthesizeKey("KEY_ArrowDown"); + // value should not update just on selection + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("form autocomplete off", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete2" autocomplete="off"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + restoreForm(form); + await popupByArrowDown(); + + // Check first entry + synthesizeKey("KEY_ArrowDown"); + // value should not update just on selection + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("username and password autocomplete off", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete2"> + <input type="text" name="uname" autocomplete="off"> + <input type="password" name="pword" autocomplete="off"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + restoreForm(form); + await popupByArrowDown(); + + // Check first entry + synthesizeKey("KEY_ArrowDown"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("changing username does not touch password", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete2"> + <input type="text" name="uname" autocomplete="off"> + <input type="password" name="pword" autocomplete="off"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + + // Test that the password field remains filled in after changing + // the username. + form.uname.focus(); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X", {shiftKey: true}); + // Trigger the 'blur' event on uname + form.pword.focus(); + is(form.uname.value, "userX", "username is set"); + is(form.pword.value, "pass", "password is set"); + }); + + add_named_task("additional username field in between", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete3", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete3", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete3"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + + // Insert a new username field into the form. We'll then make sure + // that invoking the autocomplete doesn't try to fill the form. + const newField = document.createElement("input"); + newField.setAttribute("type", "text"); + newField.setAttribute("name", "uname2"); + form.insertBefore(newField, form.pword); + is(newField.value, "", "Verifying empty uname2"); + }); + + add_named_task("additional username field in between and form reset", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete3", null, "user", "pass", "uname", "pword"] + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete3"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + + // Insert a new username field into the form. We'll then make sure + // that invoking the autocomplete doesn't try to fill the form. + const newField = document.createElement("input"); + newField.setAttribute("type", "text"); + newField.setAttribute("name", "uname2"); + form.insertBefore(newField, form.pword); + + restoreForm(form); + const autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, + ["user"], + window.location.host, + "Check dropdown is showing all logins while field is blank"); + + + // Check first entry + synthesizeKey("KEY_ArrowDown"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + // The form changes, so we expect the old username field to get the + // selected autocomplete value, but neither the new username field nor + // the password field should have any values filled in. + await SimpleTest.promiseWaitForCondition(() => form.uname.value == "user", + "Wait for username to get filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "", "password is empty"); + is(newField.value, "", "Verifying empty uname2"); + }); + + add_named_task("two forms with different actions", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete", null, "user", "pass", "uname", "pword"] + ); + const div = setContentForTask(`<div> + <form id="form1" action="https://autocomplete-other"> + <input type="email" name="uname"> + <input type="password" name="pword"> + </form> + <form id="form2" action="https://autocomplete"> + <input type="email" name="uname"> + <input type="password" name="pword"> + </form> + </div>`); + const form1 = div.querySelector("#form1"); + const form2 = div.querySelector("#form2"); + const autofillResult = await formAutofillResult(form1.id); + is(autofillResult, "no_saved_logins", "form has not been filled due to no saved logins"); + const autofillResult2 = await formAutofillResult(form2.id); + is(autofillResult2, "filled", "form has been filled"); + + is(form2.uname.value, "user", "username is set"); + is(form2.pword.value, "pass", "password is set"); + + restoreForm(form2); + is(form2.uname.value, "", "username is empty"); + is(form2.pword.value, "", "password is empty"); + + form1.uname.focus(); + is(form2.uname.value, "", "username is empty"); + is(form2.pword.value, "", "password is empty"); + }); + + add_named_task("filtering", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete", null, "form9userAB", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete", null, "form9userAAB", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + + const results = await popupBy(() => form.uname.focus()); + checkAutoCompleteResults(results, + ["form9userAAB", "form9userAB"], + window.location.host, + "Check dropdown is showing all logins while field is blank"); + synthesizeKey("KEY_Escape"); // Need to close the popup so we can get another popupshown after sending the string below. + + const results2 = await popupBy(() => sendString("form9userAB")); + checkAutoCompleteResults(results2, + ["form9userAB"], + window.location.host, + "Check dropdown is showing login with only one 'A'"); + + is(form.uname.value, "form9userAB", "username is set"); + is(form.pword.value, "", "password is empty"); + form.uname.focus(); + synthesizeKey("KEY_ArrowLeft"); + const results3 = await popupBy(() => synthesizeKey("A", {shiftKey: true})); + + is(form.uname.value, "form9userAAB", "username is set"); + is(form.pword.value, "", "password is empty"); + checkAutoCompleteResults(results3, ["form9userAAB"], + window.location.host, "Check dropdown is updated after inserting 'A'"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "form9userAAB", "username is set"); + is(form.pword.value, "pass-2", "password set"); + }); + + add_named_task("autocomplete cache", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "form9userAB", "pass-1", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "form9userAAB", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + + await popupBy(() => form.uname.focus()); + + await addLoginsInParent( + [location.origin, "https://autocomplete", null, "form9userAABzz", "pass-3", "uname", "pword"] + ); + + const promise1 = notifyMenuChanged(1); + sendString("z"); + const results1 = await promise1; + checkAutoCompleteResults(results1, [], window.location.host, + "Check popup does not have any login items"); + + // check that empty results are cached - bug 496466 + const promise2 = notifyMenuChanged(1); + sendString("z"); + const results2 = await promise2; + checkAutoCompleteResults(results2, [], window.location.host, + "Check popup only has the footer when it opens"); + }); + + add_named_task("formless", async () => { + await setStoredLoginsDuringTask( + [location.origin, location.origin, null, "user", "pass", "uname", "pword"] + ); + const form = setContentForTask(`<form id="form1"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + + // Test form-less autocomplete + // Test form-less autocomplete + // TODO: wait - whats formless about this form? + restoreForm(form); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("open on trusted focus", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user", "pass", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + + form.uname.value = ""; + form.pword.value = ""; + + // Move focus to the password field so we can test the first click on the + // username field. + form.pword.focus(); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const firePrivEventPromise = new Promise((resolve) => { + form.uname.addEventListener("click", (e) => { + ok(e.isTrusted, "Ensure event is trusted"); + resolve(); + }); + }); + await popupBy(async () => { + synthesizeMouseAtCenter(form.uname, {}); + await firePrivEventPromise; + }); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("recipes", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "user", "pass", "uname", "pword"] + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete:8888"> + <input type="text" name="uname"> + <input type="text" name="pword"> + </form>`); + + await loadRecipes({ + siteRecipes: [{ + "hosts": [window.location.host], + "usernameSelector": "input[name='1']", + "passwordSelector": "input[name='2']", + }], + }); + + // Switch the password field to type=password so _fillForm marks the username + // field for autocomplete. + form.pword.type = "password"; + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + restoreForm(form); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + + // Now test recipes with blur on the username field. + restoreForm(form); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + form.uname.value = "user"; + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Tab"); + const autofillResult3 = await formAutofillResult(form.id); + is(autofillResult3, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + await resetRecipes(); + }); + + add_named_task("form stays open upon empty search", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete:8888", null, "", "pass", "", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete:8888"> + <input type="text" name="uname" value="prefilled"> + <input type="password" name="pword" value="prefilled"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "existing_password", "form has not been filled due to existing password"); + + is(form.uname.value, "prefilled", "username is not changed"); + is(form.pword.value, "prefilled", "password is not changed"); + + form.uname.scrollIntoView(); + await popupBy(() => synthesizeMouseAtCenter(form.uname, {})); + form.uname.select(); + synthesizeKey("KEY_Delete"); + + const popupState = await getPopupState(); + is(popupState.open, true, "Check popup is still open"); + is(form.uname.value, "", "username is emoty"); + is(form.pword.value, "prefilled", "password is not changed"); + + info("testing password field"); + synthesizeMouseAtCenter(form.pword, {}); + form.pword.select(); + const popupState2 = await getPopupState(); + is(popupState2.open, false, "Check popup closed since password field isn't empty"); + await popupBy(() => synthesizeKey("KEY_Delete")); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("username only", async () => { + await setStoredLoginsDuringTask( + [location.origin, "https://autocomplete", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="https://autocomplete" autocomplete="off"> + <input type="email" name="uname" value="prefilled" autocomplete="username"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "existing_username", "form has not been filled due to existing username"); + + is(form.uname.value, "prefilled", "username is not changed"); + is(form.pword.value, "", "password is empty"); + restoreForm(form); + await popupByArrowDown(); + + // Check first entry + synthesizeKey("KEY_ArrowDown"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is set"); + is(form.pword.value, "pass", "password is set"); + }); + + add_named_task("webauthn", async () => { + await setStoredLoginsDuringTask(); + const form = setContentForTask(`<form id="form1" action="https://autocomplete"> + <input type="text" name="uname" autocomplete="webauthn"> + <input type="password" name="pword" autocomplete="current-password webauthn"> + </form>`); + + // Set up a virtual authenticator with a credential for this site. + let authenticatorId = await addVirtualAuthenticator(); + addCredential(authenticatorId, document.domain); + + // Start a conditionally mediated WebAuthn request. + let challenge = crypto.getRandomValues(new Uint8Array(16)); + let publicKey = { challenge, rpId: document.domain }; + let webauthnPromise = navigator.credentials.get( { publicKey, mediation: "conditional" }); + + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "no_saved_logins", "form has not been filled due to no saved logins"); + + // Ensure that focusing on either input shows the popup + form.uname.focus(); + await popupByArrowDown(); + + form.pword.focus(); + await popupByArrowDown(); + + // Check first entry + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + + let credential = await webauthnPromise; + is(credential.type, "public-key", "received a webauthn credential"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_formActionOrigin.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_formActionOrigin.html new file mode 100644 index 0000000000..0365d5369c --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_formActionOrigin.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test that logins with non-matching formActionOrigin appear in autocomplete dropdown</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Login Manager test: logins with non-matching formActionOrigin appear in autocomplete dropdown + +<template id="form1-template"> + <form id="form1"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const formTemplate = document.getElementById("form1-template"); + + add_setup(async () => { + await setStoredLoginsDuringTest( + [window.location.origin, "https://differentFormSubmitURL", null, "dfsu1", "dfsp1", "uname", "pword"] + ); + listenForUnexpectedPopupShown(); + }); + + add_named_task("form initially empty", async () => { + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "no_saved_logins", "form has not been filled due to no saved logins"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const popupState = await getPopupState(); + is(popupState.open, false, "Check popup is initially closed"); + }); + + /* For this testcase, the only login that exists for this origin + * is one with a different formActionOrigin, so the login will appear + * in the autocomplete popup. + */ + add_named_task("menu shows logins for different form action origin", async () => { + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "no_saved_logins", "form has not been filled due to no saved logins"); + + // Trigger autocomplete popup + form.uname.focus(); + + const autocompleteItems = await popupByArrowDown(); + + const popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + + const expectedMenuItems = ["dfsu1"]; + checkAutoCompleteResults(autocompleteItems, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly."); + + synthesizeKey("KEY_ArrowDown"); // first item + + // value shouldn't update just by selecting + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "dfsu1", "username is set"); + is(form.pword.value, "dfsp1", "password is set"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_insecure.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_insecure.html new file mode 100644 index 0000000000..156a89f77a --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_insecure.html @@ -0,0 +1,932 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test insecure form field autocomplete</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Test for Login Manager: login autocomplete, with insecure connection. This tests autocomplete menu items, its navigation, the selection and deletion of entries as well as sending untrusted events. + +This tests the login manager in a insecure setting using http. A similar test file exists for using a secure connection: test_autocomplete_basic_form.html. + +<template id="form1-template"> + <form id="form1" action="http://autocomplete:8888/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const formTemplate = document.getElementById("form1-template"); + + // Restore the form to the default state. + function restoreForm(form) { + form.uname.value = ""; + form.pword.value = ""; + form.uname.focus(); + } + + add_setup(async () => { + listenForUnexpectedPopupShown(); + }); + + add_named_task("form is initially empty and popup closed", async () => { + const form = setContentForTask(formTemplate); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const popupState = await getPopupState(); + is(popupState.open, false, "Check popup is closed"); + }); + + add_named_task("menuitems, telemetry events, selection and escape", async () => { + await setStoredLoginsDuringTask( + // login 0 has no username, so should be filtered out from the autocomplete list. + [location.origin, "http://autocomplete:8888", null, "", "pass0", "", "pword"], + + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + form.uname.focus(); + const autocompleteItems = await popupByArrowDown(); + + const popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + + const expectedMenuItems = [ + "This connection is not secure. Logins entered here could be compromised. Learn More", + "user-1", + "user-2", + "user-3" + ]; + checkAutoCompleteResults(autocompleteItems, expectedMenuItems, + window.location.host, "Check all menuitems are displayed correctly."); + + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Escape"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("select first entry", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + const popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); // first + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "pass-1", "password is set"); + }); + + add_named_task("select second entry", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowDown"); // second + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("wrap around first entry", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowDown"); // second + synthesizeKey("KEY_ArrowDown"); // footer + synthesizeKey("KEY_ArrowDown"); // deselects + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "pass-1", "password is set"); + }); + + add_named_task("wrap around up last entry", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_ArrowUp"); // last (fourth) + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("wrap around up down up up", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); // select first entry + synthesizeKey("KEY_ArrowUp"); // selects nothing! + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_ArrowUp"); // select last entry + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("wrap around up down up last", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowUp"); // deselects + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_ArrowUp"); // last entry + synthesizeKey("KEY_ArrowUp"); // skip insecure warning + synthesizeKey("KEY_ArrowUp"); // first entry + synthesizeKey("KEY_ArrowUp"); // deselects + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_ArrowUp"); // last entry + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("fill username without autofill right", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + // Set first entry w/o triggering autocomplete + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowRight"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("fill username without autofill left", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + // Set first entry w/o triggering autocomplete + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowLeft"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("page up first", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + // Check first entry (page up) + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowDown"); // second + synthesizeKey("KEY_PageUp"); // first + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "pass-1", "password is set"); + }); + + add_named_task("page down last", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + // Check last entry (page down) + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_PageDown"); // footer + synthesizeKey("KEY_ArrowUp"); // last + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("untrusted event", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("delete", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + + // XXX tried sending character "t" before/during dropdown to test + // filtering, but had no luck. Seemed like the character was getting lost. + // Setting uname.value didn't seem to work either. This works with a human + // driver, so I'm not sure what's up. + + // Delete the first entry (of 3), "user-1" + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); + const numLoginsBeforeDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null); + is(numLoginsBeforeDeletion, 3, "Correct number of logins before deleting one"); + + const countChangedPromise = notifyMenuChanged(4); + const deletionPromise = promiseStorageChanged(["removeLogin"]); + // On OS X, shift-backspace and shift-delete work, just delete does not. + // On Win/Linux, shift-backspace does not work, delete and shift-delete do. + synthesizeKey("KEY_Delete", {shiftKey: true}); + await deletionPromise; + + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const numLoginsAfterDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null); + is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one"); + await countChangedPromise; + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-2", "username is set"); + is(form.pword.value, "pass-2", "password is set"); + }); + + add_named_task("delete second", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + // Delete the second entry (of 3), "user-2" + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Delete", {shiftKey: true}); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const numLoginsAfterDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null); + is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one"); + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-3", "username is set"); + is(form.pword.value, "pass-3", "password is set"); + }); + + add_named_task("delete last", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-2", "pass-2", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "user-3", "pass-3", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + form.uname.focus(); + await popupByArrowDown(); + + /* test 54 */ + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + // Delete the last entry (of 3), "user-3" + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + const numLoginsBeforeDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null); + is(numLoginsBeforeDeletion, 3, "Correct number of logins before deleting one"); + synthesizeKey("KEY_Delete", {shiftKey: true}); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const numLoginsAfterDeletion = await LoginManager.countLogins(location.origin, "http://autocomplete:8888", null); + is(numLoginsAfterDeletion, 2, "Correct number of logins after deleting one"); + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user-1", "username is set"); + is(form.pword.value, "pass-1", "password is set"); + }); + + + // Tests for single-user forms for ignoring autocomplete=off + + add_named_task("default", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete2"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("password autocomplete off", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete2"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="off"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + + restoreForm(form); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + // Check first entry + synthesizeKey("KEY_ArrowDown"); + // fields should not update just on selection + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("username autocomplete off", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete2"> + <input type="text" name="uname" autocomplete="off"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + + restoreForm(form); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + // Check first entry + synthesizeKey("KEY_ArrowDown"); + // value should not update just on selection + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("form autocomplete off", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete2" autocomplete="off"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + restoreForm(form); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + // Check first entry + synthesizeKey("KEY_ArrowDown"); + // value should not update just on selection + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("username and password autocomplete off", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete2"> + <input type="text" name="uname" autocomplete="off"> + <input type="password" name="pword" autocomplete="off"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + restoreForm(form); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + // Check first entry + synthesizeKey("KEY_ArrowDown"); + // value should not update just on selection + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("changing username does not touch password", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete2", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete2"> + <input type="text" name="uname" autocomplete="off"> + <input type="password" name="pword" autocomplete="off"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + + // Test that the password field remains filled in after changing + // the username. + form.uname.focus(); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X", {shiftKey: true}); + // Trigger the 'blur' event on uname + form.pword.focus(); + is(form.uname.value, "userX", "username is set"); + is(form.pword.value, "pass", "password is set"); + }); + + add_named_task("additional username field in between", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete3", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete3", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete3"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + + // Insert a new username field into the form. We'll then make sure + // that invoking the autocomplete doesn't try to fill the form. + const newField = document.createElement("input"); + newField.setAttribute("type", "text"); + newField.setAttribute("name", "uname2"); + form.insertBefore(newField, form.pword); + is(newField.value, "", "Verifying empty uname2"); + }); + + add_named_task("additional username field in between and form reset", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete3", null, "user", "pass", "uname", "pword"] + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete3"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + + // Insert a new username field into the form. We'll then make sure + // that invoking the autocomplete doesn't try to fill the form. + const newField = document.createElement("input"); + newField.setAttribute("type", "text"); + newField.setAttribute("name", "uname2"); + form.insertBefore(newField, form.pword); + + restoreForm(form); + const autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, + [ + "This connection is not secure. Logins entered here could be compromised. Learn More", + "user" + ], + window.location.host, + "Check dropdown is showing all logins while field is blank"); + + + // Check first entry + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + // The form changes, so we expect the old username field to get the + // selected autocomplete value, but neither the new username field nor + // the password field should have any values filled in. + await SimpleTest.promiseWaitForCondition(() => form.uname.value == "user", + "Wait for username to get filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "", "password is empty"); + is(newField.value, "", "Verifying empty uname2"); + }); + + add_named_task("two forms with different actions", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete", null, "user", "pass", "uname", "pword"] + ); + const div = setContentForTask(`<div> + <form id="form1" action="http://autocomplete-other"> + <input type="email" name="uname"> + <input type="password" name="pword"> + </form> + <form id="form2" action="http://autocomplete"> + <input type="email" name="uname"> + <input type="password" name="pword"> + </form> + </div>`); + const form1 = div.querySelector("#form1"); + const form2 = div.querySelector("#form2"); + const autofillResult = await formAutofillResult(form1.id); + is(autofillResult, "no_saved_logins", "form has not been filled due to no saved logins"); + const autofillResult2 = await formAutofillResult(form2.id); + is(autofillResult2, "filled", "form has been filled"); + + is(form2.uname.value, "user", "username is set"); + is(form2.pword.value, "pass", "password is set"); + + restoreForm(form2); + is(form2.uname.value, "", "username is empty"); + is(form2.pword.value, "", "password is empty"); + + form1.uname.focus(); + is(form2.uname.value, "", "username is empty"); + is(form2.pword.value, "", "password is empty"); + }); + + add_named_task("filtering", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete", null, "form9userAB", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete", null, "form9userAAB", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + + const results = await popupBy(() => form.uname.focus()); + checkAutoCompleteResults(results, + [ + "This connection is not secure. Logins entered here could be compromised. Learn More", + "form9userAAB", + "form9userAB" + ], + window.location.host, + "Check dropdown is showing all logins while field is blank"); + synthesizeKey("KEY_Escape"); // Need to close the popup so we can get another popupshown after sending the string below. + + const results2 = await popupBy(() => sendString("form9userAB")); + checkAutoCompleteResults(results2, + [ + "This connection is not secure. Logins entered here could be compromised. Learn More", + "form9userAB" + ], + window.location.host, + "Check dropdown is showing login with only one 'A'"); + + is(form.uname.value, "form9userAB", "username is set"); + is(form.pword.value, "", "password is empty"); + form.uname.focus(); + synthesizeKey("KEY_ArrowLeft"); + const results3 = await popupBy(() => synthesizeKey("A", {shiftKey: true})); + + is(form.uname.value, "form9userAAB", "username is set"); + is(form.pword.value, "", "password is empty"); + checkAutoCompleteResults(results3, + [ + "This connection is not secure. Logins entered here could be compromised. Learn More", + "form9userAAB" + ], + window.location.host, "Check dropdown is updated after inserting 'A'"); + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "form9userAAB", "username is set"); + is(form.pword.value, "pass-2", "password set"); + }); + + add_named_task("autocomplete cache", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "form9userAB", "pass-1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "form9userAAB", "pass-2", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "multiple_logins", "form has not been filled due to multiple logins"); + + await popupBy(() => form.uname.focus()); + + await addLoginsInParent( + [location.origin, "https://autocomplete", null, "form9userAABzz", "pass-3", "uname", "pword"] + ); + + const promise1 = notifyMenuChanged(2); + sendString("z"); + const results1 = await promise1; + checkAutoCompleteResults(results1, [ + "This connection is not secure. Logins entered here could be compromised. Learn More", + ], window.location.host, + "Check popup does not have any login items"); + + // check that empty results are cached - bug 496466 + const promise2 = notifyMenuChanged(2); + sendString("z"); + const results2 = await promise2; + checkAutoCompleteResults(results2, [ + "This connection is not secure. Logins entered here could be compromised. Learn More", + ], window.location.host, + "Check popup only has the footer when it opens"); + }); + + add_named_task("formless", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user", "pass", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + + // Test form-less autocomplete + // TODO: wait - whats formless about this form? + restoreForm(form); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("open on trusted focus", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user", "pass", "uname", "pword"] + ); + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + + form.uname.value = ""; + form.pword.value = ""; + + // Move focus to the password field so we can test the first click on the + // username field. + form.pword.focus(); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const firePrivEventPromise = new Promise((resolve) => { + form.uname.addEventListener("click", (e) => { + ok(e.isTrusted, "Ensure event is trusted"); + resolve(); + }); + }); + await popupBy(async () => { + synthesizeMouseAtCenter(form.uname, {}); + await firePrivEventPromise; + }); + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + }); + + add_named_task("recipes", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "user", "pass", "uname", "pword"] + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete:8888"> + <input type="text" name="uname"> + <input type="text" name="pword"> + </form>`); + + await loadRecipes({ + siteRecipes: [{ + "hosts": [window.location.host], + "usernameSelector": "input[name='1']", + "passwordSelector": "input[name='2']", + }], + }); + + // Switch the password field to type=password so _fillForm marks the username + // field for autocomplete. + form.pword.type = "password"; + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + restoreForm(form); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + synthesizeKey("KEY_ArrowDown"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + + // Now test recipes with blur on the username field. + restoreForm(form); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + form.uname.value = "user"; + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Tab"); + const autofillResult3 = await formAutofillResult(form.id); + is(autofillResult3, "filled", "form has been filled"); + is(form.uname.value, "user", "username is filled"); + is(form.pword.value, "pass", "password is filled"); + await resetRecipes(); + }); + + add_named_task("form stays open upon empty search", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete:8888", null, "", "pass", "", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete:8888"> + <input type="text" name="uname" value="prefilled"> + <input type="password" name="pword" value="prefilled"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "existing_password", "form has not been filled due to existing password"); + + is(form.uname.value, "prefilled", "username is not changed"); + is(form.pword.value, "prefilled", "password is not changed"); + + form.uname.scrollIntoView(); + await popupBy(() => synthesizeMouseAtCenter(form.uname, {})); + form.uname.select(); + synthesizeKey("KEY_Delete"); + + const popupState = await getPopupState(); + is(popupState.open, true, "Check popup is still open"); + is(form.uname.value, "", "username is emoty"); + is(form.pword.value, "prefilled", "password is not changed"); + + info("testing password field"); + synthesizeMouseAtCenter(form.pword, {}); + form.pword.select(); + const popupState2 = await getPopupState(); + is(popupState2.open, false, "Check popup closed since password field isn't empty"); + await popupBy(() => synthesizeKey("KEY_Delete")); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + }); + + add_named_task("username only", async () => { + await setStoredLoginsDuringTask( + [location.origin, "http://autocomplete", null, "user", "pass", "uname", "pword"], + ); + const form = setContentForTask(`<form id="form1" action="http://autocomplete" autocomplete="off"> + <input type="email" name="uname" value="prefilled" autocomplete="username"> + <input type="password" name="pword"> + </form>`); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "existing_username", "form has not been filled due to existing username"); + + is(form.uname.value, "prefilled", "username is not changed"); + is(form.pword.value, "", "password is empty"); + restoreForm(form); + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); // skip insecure warning + // Check first entry + synthesizeKey("KEY_ArrowDown"); + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + synthesizeKey("KEY_Enter"); + await untilAutocompletePopupClosed(); + is(form.uname.value, "user", "username is set"); + is(form.pword.value, "pass", "password is set"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html new file mode 100644 index 0000000000..1dcd8b116e --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test login autocomplete with related realms</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Login Manager test: related realms autocomplete + +<template id="form1-template"> + <form id="form1" action="https://autocomplete:8888/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const formTemplate = document.getElementById("form1-template"); + + add_setup(async () => { + await setStoredLoginsDuringTest( + // Simple related domain relationship where example.com and other-example.com are in the related domains list + ["https://other-example.com", "https://other-example.com", null, "relatedUser1", "relatedPass1", "uname", "pword"], + + // Example.com and example.co.uk are related, so sub.example.co.uk should appear on example.com's autocomplete dropdown + // The intent is to cover the ebay.com/ebay.co.uk and all other country TLD cases + // where the sign in page is actually signin.ebay.com/signin.ebay.co.uk but credentials could have manually been entered + // for ebay.com/ebay.co.uk or automatically stored as signin.ebay.com/sigin.ebay.co.uk + ["https://sub.example.co.uk", "https://sub.example.co.uk", null, "subUser1", "subPass1", "uname", "pword"], + ); + listenForUnexpectedPopupShown(); + }); + + add_named_task("form is initially empty", async () => { + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "no_saved_logins", "form has not been filled due to no saved logins"); + + // Make sure initial form is empty. + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + const popupState = await getPopupState(); + is(popupState.open, false, "Check popup is initially closed"); + }); + + add_named_task("form related domain menuitems", async () => { + const form = setContentForTask(formTemplate); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "no_saved_logins", "form has not been filled due to no saved logins"); + + form.uname.focus(); + + const autocompleteItems = await popupByArrowDown(); + const popupState = await getPopupState(); + + is(popupState.selectedIndex, -1, "Check no entires are selected upon opening"); + + const expectedMenuItems = ["relatedUser1", "subUser1"]; + checkAutoCompleteResults(autocompleteItems, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly"); + + const acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }); + is(acEvents.length, 1, "One autocomplete event"); + checkACTelemetryEvent(acEvents[0], form.uname, { + "hadPrevious": "0", + "login": expectedMenuItems.length + "", + "loginsFooter": "1" + }); + checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update just by opening + + synthesizeKey("KEY_ArrowDown"); // first item + // value shouldn't update just by selecting + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + + synthesizeKey("KEY_Enter"); + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + is(form.pword.value, "relatedPass1", "password should match the login that was selected"); + checkLoginForm(form.uname, "relatedUser1", form.pword, "relatedPass1"); + is(form.uname.value, "relatedUser1", "username is set"); + is(form.pword.value, "relatedPass1", "password is set"); + + form.uname.value = ""; + form.pword.value = ""; + form.uname.focus(); + + await popupByArrowDown(); + + synthesizeKey("KEY_ArrowDown"); // first item + synthesizeKey("KEY_ArrowDown"); // second item + // value shouldn't update just by selecting + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + + synthesizeKey("KEY_Enter"); + const autofillResult3 = await formAutofillResult(form.id); + is(autofillResult3, "filled", "form has been filled"); + is(form.uname.value, "subUser1", "username is set"); + is(form.pword.value, "subPass1", "password is set"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_hasBeenTypePassword.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_hasBeenTypePassword.html new file mode 100644 index 0000000000..0eb2beadc7 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_hasBeenTypePassword.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test that passwords are autocompleted into fields that were previously type=password</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Login Manager test: Test that passwords are autocompleted into fields that were +previously type=password + +Usually the autocomplete login form only operates on password fields which are +of type `password`. But when a password fields type has been changed to `text` +via JavaScript, the password manager should still fill this form, because this +is often used in real world forms to handle masking/unmasking of password +fields. + +<template id="form1-template"> + <form id="form1" action="https://www.example.com/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const formTemplate = document.getElementById("form1-template"); + + add_setup(async () => { + const origin = window.location.origin; + await setStoredLoginsDuringTest( + [origin, origin, null, "user1", "pass1"], + [origin, origin, null, "user2", "pass2"] + ); + listenForUnexpectedPopupShown(); + }); + + add_named_task("autofill operates on a password field made text", async () => { + const form = setContentForTask(formTemplate); + + // initial consume autofill event + await formAutofillResult(form.id); + + info("Setting the password field type to text"); + // This is similar to a site implementing their own password visibility/unmasking toggle + form.pword.type = "text"; + + // Trigger autocomplete popup + form.uname.focus(); + const autocompleteItems = await popupByArrowDown(); + + const popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + + checkAutoCompleteResults(autocompleteItems, ["user1", "user2"], window.location.host, + "Check all menuitems are displayed correctly."); + + synthesizeKey("KEY_ArrowDown"); // first item + // value shouldn't update just by selecting + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + + synthesizeKey("KEY_Enter"); + + const autofillResult1 = await formAutofillResult(form.id); + is(autofillResult1, "filled", "form has been filled"); + + is(form.uname.value, "user1", "username should match the login"); + is(form.pword.value, "pass1", "password should match the login"); + + form.reset(); + + form.pword.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); // first item + // value shouldn't update just by selecting + is(form.uname.value, "", "username is empty"); + is(form.pword.value, "", "password is empty"); + + synthesizeKey("KEY_Enter"); + is(form.uname.value, "", "username should stay empty"); + is(form.pword.value, "pass1", "Password should match the login that was selected"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight.html new file mode 100644 index 0000000000..49cedde0bc --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test form field autofill highlight</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +<script> +const { ContentTaskUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" +); + +add_setup(async () => { + await setStoredLoginsAsync( + [location.origin, "https://autocomplete", null, "user1", "pass1", "", ""], + [location.origin, "https://autocomplete", null, "user2", "pass2", "", ""] + ); +}); + +add_task(async function test_field_highlight_on_autocomplete() { + const form = createLoginForm({ + action: "https://autocomplete" + }); + + await openPopupOn(form.uname); + synthesizeKey("KEY_ArrowDown"); + await synthesizeKey("KEY_Enter"); + + await ContentTaskUtils.waitForCondition(() => { + return form.uname.matches(":autofill") + }, "Highlight was successfully applied to the username field on username autocomplete"); + + ok(form.pword.matches(":autofill"), + "Highlight was successfully applied to the password field on username autocomplete"); + + // Clear existing highlight on login fields. We check by pressing the tab key after backspace + // (by shifting focus to the next element) because the tab key was known to cause a bug where the + // highlight is applied once again. See Bug 1526522. + form.uname.focus(); + synthesizeKey("KEY_Backspace"); + synthesizeKey("KEY_Tab"); + ok(!form.uname.matches(":autofill"), + "Highlight was successfully removed on the username field"); + + synthesizeKey("KEY_Backspace"); + synthesizeKey("KEY_Tab"); + ok(!form.pword.matches(":autofill"), + "Highlight was successfully removed on the password field"); + + // Clear login fields. + form.uname.value = ""; + form.pword.value = ""; + + // Test password field autocomplete. + await openPopupOn(form.pword); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + + await ContentTaskUtils.waitForCondition(() => { + return form.pword.matches(":autofill"); + }, "Highlight was successfully applied to the password field on password autocomplete"); + + // Clear existing highlight on the password field. We check by pressing the tab key after backspace + // (by shifting focus to the next element) because the tab key was known to cause a bug where the + // highlight is applied once again. See Bug 1526522. + synthesizeKey("KEY_Backspace"); + synthesizeKey("KEY_Tab"); + + ok(!form.pword.matches(":autofill"), + "Highlight was successfully removed on the password field"); + + // Clear login fields. + form.uname.value = ""; + form.pword.value = ""; +}); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_non_login.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_non_login.html new file mode 100644 index 0000000000..eb82e90656 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_non_login.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test form field autofill highlight</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +<script> +function closeCurrentTab() { + runInParent(function cleanUpWindow() { + let window = Services.wm.getMostRecentWindow("navigator:browser"); + window.gBrowser.removeTab(window.gBrowser.selectedTab); + }); +} + +add_setup(async () => { + await setStoredLoginsAsync( + [location.origin, "http://autocomplete", null, "user1", "pass1", "", ""], + [location.origin, "http://autocomplete", null, "user2", "pass2", "", ""] + ); +}); + +add_task(async function test_field_highlight_on_pw_field_autocomplete_insecureWarning() { + const form = createLoginForm({ + action: "http://autocomplete" + }); + await promiseFormsProcessedInSameProcess(); + + // Press enter on insecure warning and check. + form.pword.focus(); + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); // insecure warning + synthesizeKey("KEY_Enter"); + + is(document.defaultView.getComputedStyle(form.pword).getPropertyValue("filter"), "none", + "Highlight is not applied to the password field if enter key is pressed on the insecure warning item"); + is(document.defaultView.getComputedStyle(form.uname).getPropertyValue("filter"), "none", + "Highlight is not applied to the username field if enter key is pressed on the insecure warning item"); + + // Press tab on insecure warning and check. + await openPopupOn(form.pword); + synthesizeKey("KEY_ArrowDown"); // insecure warning + synthesizeKey("KEY_Tab"); + + is(document.defaultView.getComputedStyle(form.pword).getPropertyValue("filter"), "none", + "Highlight is not applied to the password field if tab key is pressed on the insecure warning item"); + is(document.defaultView.getComputedStyle(form.uname).getPropertyValue("filter"), "none", + "Highlight is not applied to the username field if tab key is pressed on the insecure warning item"); +}); + +add_task(async function test_field_highlight_on_pw_field_autocomplete_footer() { + const form = createLoginForm({ + action: "http://autocomplete" + }); + await promiseFormsProcessedInSameProcess(); + + // Press enter on the footer and check. + await openPopupOn(form.pword); + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_Enter"); + + is(document.defaultView.getComputedStyle(form.pword).getPropertyValue("filter"), "none", + "Highlight is not applied to the password field if enter key is pressed on the footer item"); + is(document.defaultView.getComputedStyle(form.uname).getPropertyValue("filter"), "none", + "Highlight is not applied to the username field if enter key is pressed on the footer item"); + + closeCurrentTab(); + + // Press tab on the footer and check. + await openPopupOn(form.pword); + synthesizeKey("KEY_ArrowUp"); // footer + synthesizeKey("KEY_Tab"); + + is(document.defaultView.getComputedStyle(form.pword).getPropertyValue("filter"), "none", + "Highlight is not applied to the password field if tab key is pressed on the footer item"); + is(document.defaultView.getComputedStyle(form.uname).getPropertyValue("filter"), "none", + "Highlight is not applied to the username field if tab key is pressed on the insecure warning item"); + + closeCurrentTab(); +}); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_username_only_form.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_username_only_form.html new file mode 100644 index 0000000000..2451f47308 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_highlight_username_only_form.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test form field autofill highlight</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +<script> +const { ContentTaskUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" +); + +add_setup(async () => { + await setStoredLoginsAsync( + [location.origin, "https://autocomplete", null, "user1", "pass1", "", ""], + [location.origin, "https://autocomplete", null, "user2", "pass2", "", ""] + ); +}); + +add_task(async function test_username_field_in_username_only_form_highlight_on_autocomplete() { + const form = createLoginForm({ + action: "https://autocomplete", + username: { + autocomplete: "username" + }, + password: false + }); + + await openPopupOn(form.uname); + synthesizeKey("KEY_ArrowDown"); + await synthesizeKey("KEY_Enter"); + + await ContentTaskUtils.waitForCondition(() => { + return form.uname.matches(":autofill"); + }, "Highlight was successfully applied to the username field on username autocomplete"); + + // Clear existing highlight on login fields. We check by pressing the tab key after backspace + // (by shifting focus to the next element) because the tab key was known to cause a bug where the + // highlight is applied once again. See Bug 1526522. + form.uname.focus(); + synthesizeKey("KEY_Backspace"); + synthesizeKey("KEY_Tab"); + ok(!form.uname.matches(":autofill"), + "Highlight was successfully removed on the username field"); +}); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_downgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_downgrade.html new file mode 100644 index 0000000000..44d830afe5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_downgrade.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"> + <iframe></iframe> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +const origin = "http://" + window.location.host; +const secureOrigin = "https://" + window.location.host; +const iframe = document.getElementsByTagName("iframe")[0]; +let iframeDoc, hostname; +let uname; +let pword; + +// Restore the form to the default state. +function restoreForm() { + return SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + this.content.document.getElementById("form-basic-password").focus(); + this.content.document.getElementById("form-basic-username").value = ""; + this.content.document.getElementById("form-basic-password").value = ""; + this.content.document.getElementById("form-basic-username").focus(); + }); +} + +const HTTP_FORM_URL = origin + "/tests/toolkit/components/passwordmgr/test/mochitest/form_basic.html"; + +add_setup(async () => { + await setStoredLoginsAsync( + // We have two actual HTTPS to avoid autofill before the schemeUpgrades pref flips to true. + [secureOrigin, secureOrigin, null, "name", "pass", "uname", "pword"], + [secureOrigin, secureOrigin, null, "name1", "pass1", "uname", "pword"], + // Same as above but HTTP instead of HTTPS (to test de-duping) + [origin, origin, null, "name1", "pass1", "uname", "pword"], + // Different HTTP login to upgrade with secure formActionOrigin + [origin, secureOrigin, null, "name2", "passHTTPtoHTTPS", "uname", "pword"] + ); +}); + +async function setup(formUrl) { + await SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]}); + + let processedPromise = promiseFormsProcessed(); + iframe.src = formUrl; + await new Promise(resolve => { + iframe.addEventListener("load", function() { + resolve(); + }, {once: true}); + }); + + await processedPromise; + + hostname = await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + return this.content.document.documentURIObject.host; + }); +} + +add_task(async function test_autocomplete_https_downgrade() { + info("test_autocomplete_http, setup with " + HTTP_FORM_URL); + await setup(HTTP_FORM_URL); + + let logins = await LoginManager.getAllLogins(); + info("got logins: " + logins.map(l => l.origin)); + + // from a HTTP page, look for matching logins, we should never offer a login with an HTTPS scheme + // we're expecting just login2 as a match + let isCrossOrigin = false; + try { + // If this is a cross-origin test, the parent will be inaccessible. The fields + // should not be filled in. + window.parent.windowGlobalChild; + } catch(ex) { + isCrossOrigin = true; + } + + await checkLoginFormInFrame(iframe, "form-basic-username", isCrossOrigin ? "" : "name1", + "form-basic-password", isCrossOrigin ? "" : "pass1"); + + // Trigger autocomplete popup + await restoreForm(); + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is initially closed"); + const autocompleteItems = await popupByArrowDown(); + info("got results: " + autocompleteItems.join(", ")); + popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected"); + checkAutoCompleteResults(autocompleteItems, ["This connection is not secure. Logins entered here could be compromised. Learn More", "name1", "name2"], hostname, "initial"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html new file mode 100644 index 0000000000..a0a81a8c88 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_https_upgrade.html @@ -0,0 +1,191 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autocomplete on an HTTPS page using upgraded HTTP logins</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"> + <iframe></iframe> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +const originSecure = "https://" + window.location.host; +const originNonSecure = "http://" + window.location.host; +const iframe = document.getElementsByTagName("iframe")[0]; +let iframeDoc, hostname; + +// Restore the form to the default state. +function restoreForm() { + return SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + this.content.document.getElementById("form-basic-password").focus(); + this.content.document.getElementById("form-basic-username").value = ""; + this.content.document.getElementById("form-basic-password").value = ""; + this.content.document.getElementById("form-basic-username").focus(); + }); +} + +const HTTPS_FORM_URL = originSecure + "/tests/toolkit/components/passwordmgr/test/mochitest/form_basic.html"; + +async function setup(formUrl = HTTPS_FORM_URL) { + await SpecialPowers.pushPrefEnv({"set": [["signon.schemeUpgrades", true]]}); + + let processedPromise = promiseFormsProcessed(); + iframe.src = formUrl; + await new Promise(resolve => { + iframe.addEventListener("load", function() { + resolve(); + }, {once: true}); + }); + + await processedPromise; + + hostname = await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + return this.content.document.documentURIObject.host; + }); +} + +add_setup(async () => { + await setStoredLoginsAsync( + // We have two actual HTTPS to avoid autofill before the schemeUpgrades pref flips to true. + [originSecure, originSecure, null, "name", "pass", "uname", "pword"], + [originSecure, originSecure, null, "name1", "pass1", "uname", "pword"], + + // Same as above but HTTP instead of HTTPS (to test de-duping) + [originNonSecure, originNonSecure, null, "name1", "pass1", "uname", "pword"], + + // Different HTTP login to upgrade with secure formActionOrigin + [originNonSecure, originSecure, null, "name2", "passHTTPtoHTTPS", "uname", "pword"] + ); +}); + +add_task(async function setup_https_frame() { + await setup(HTTPS_FORM_URL); +}); + +add_task(async function test_empty_first_entry() { + // Make sure initial form is empty. + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", ""); + // Trigger autocomplete popup + await restoreForm(); + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is initially closed"); + let autocompleteItems = await popupBy(); + popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected"); + checkAutoCompleteResults(autocompleteItems, ["name", "name1", "name2"], hostname, "initial"); + + // Check first entry + let index0Promise = notifySelectedIndex(0); + synthesizeKey("KEY_ArrowDown"); + await index0Promise; + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", ""); // value shouldn't update + synthesizeKey("KEY_Enter"); + await promiseFormsProcessedInSameProcess(); + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name", "form-basic-password", "pass"); +}); + +add_task(async function test_empty_second_entry() { + await restoreForm(); + await popupBy(); + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowDown"); // second + synthesizeKey("KEY_Enter"); + await promiseFormsProcessedInSameProcess(); + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name1", "form-basic-password", "pass1"); +}); + +add_task(async function test_search() { + await restoreForm(); + let results = await popupBy(async () => { + // We need to blur for the autocomplete controller to notice the forced value below. + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + let uname = this.content.document.getElementById("form-basic-username"); + uname.blur(); + uname.value = "name"; + uname.focus(); + }); + + sendChar("1"); + synthesizeKey("KEY_ArrowDown"); // open + }); + checkAutoCompleteResults(results, ["name1"], hostname, "check result deduping for 'name1'"); + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_Enter"); + await promiseFormsProcessedInSameProcess(); + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "name1", "form-basic-password", "pass1"); + + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is now closed"); +}); + +add_task(async function test_delete_first_entry() { + await restoreForm(); + await popupBy(); + + let index0Promise = notifySelectedIndex(0); + synthesizeKey("KEY_ArrowDown"); + await index0Promise; + + let deletionPromise = promiseStorageChanged(["removeLogin"]); + // On OS X, shift-backspace and shift-delete work, just delete does not. + // On Win/Linux, shift-backspace does not work, delete and shift-delete do. + synthesizeKey("KEY_Delete", {shiftKey: true}); + await deletionPromise; + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", ""); + + let results = await notifyMenuChanged(3, "name1"); + + checkAutoCompleteResults(results, ["name1", "name2"], hostname, "two logins should remain after deleting the first"); + let popupState = await getPopupState(); + is(popupState.open, true, "Check popup stays open after deleting"); + synthesizeKey("KEY_Escape"); + popupState = await getPopupState(); + is(popupState.open, false, "Check popup closed upon ESC"); +}); + +add_task(async function test_delete_duplicate_entry() { + await restoreForm(); + await popupBy(); + + let index0Promise = notifySelectedIndex(0); + synthesizeKey("KEY_ArrowDown"); + await index0Promise; + + let deletionPromise = promiseStorageChanged(["removeLogin"]); + // On OS X, shift-backspace and shift-delete work, just delete does not. + // On Win/Linux, shift-backspace does not work, delete and shift-delete do. + synthesizeKey("KEY_Delete", {shiftKey: true}); + await deletionPromise; + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), "form-basic-username", "", "form-basic-password", ""); + + is(await LoginManager.countLogins(originNonSecure, originNonSecure, null), 1, + "Check that the HTTP login remains"); + is(await LoginManager.countLogins(originSecure, originSecure, null), 0, + "Check that the HTTPS login was deleted"); + + // Two menu items should remain as the HTTPS login should have been deleted but + // the HTTP would remain. + let results = await notifyMenuChanged(2, "name2"); + + checkAutoCompleteResults(results, ["name2"], hostname, "one login should remain after deleting the HTTPS name1"); + let popupState = await getPopupState(); + is(popupState.open, true, "Check popup stays open after deleting"); + synthesizeKey("KEY_Escape"); + popupState = await getPopupState(); + is(popupState.open, false, "Check popup closed upon ESC"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html new file mode 100644 index 0000000000..3fd8b1e90b --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html @@ -0,0 +1,596 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofill and autocomplete on autocomplete=new-password fields</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="pwmgr_common.js"></script> + <script src="../../../satchel/test/satchel_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Login Manager test: autofill with password generation on `autocomplete=new-password` fields. + +<template id="form1-template"> + <form id="form1" action="https://autofill"> + <input type="text" name="uname"> + <input type="password" name="p"> + <button type="submit" name="submit">Submit</button> + </form> +</template> + +<template id="form2-template"> + <form id="form2" action="https://autofill"> + <input type="text" name="uname"> + <input type="password" name="password" autocomplete="new-password"> + <button type="submit" name="submit">Submit</button> + </form> +</template> + +<template id="form3-template"> + <form id="form3" action="https://autofill"> + <input type="text" name="username"> + <label>New password<input type="password" name="password"></label> + <button type="submit" name="submit">Submit</button> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const { TestUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" + ); + + const formTemplates = { + form1: document.getElementById("form1-template"), + form2: document.getElementById("form2-template"), + form3: document.getElementById("form3-template"), + }; + + const dateAndTimeFormatter = new SpecialPowers.Services.intl.DateTimeFormat(undefined, { + dateStyle: "medium", + }); + + async function showACPopup(formNumber, expectedACLabels) { + const autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, expectedACLabels, + window.location.host, "Check all rows are correct"); + } + + function clearGeneratedPasswords() { + const { LoginManagerParent } = ChromeUtils.importESModule( + "resource://gre/modules/LoginManagerParent.sys.mjs" + ); + if (LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin()) { + LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); + } + } + + add_named_task("autofill autocomplete username no generation", async () => { + await setPreferencesForTask( + ["signon.generation.available", false], + ["signon.generation.enabled", false], + ); + await setStoredLoginsDuringTask([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form1 = setContentForTask(formTemplates.form1); + + const autofillResult1 = await formAutofillResult(form1.id); + is(autofillResult1, "filled", "form has not been filled due to multiple logins"); + + // reference form was filled as expected? + is(form1.uname.value, "user1", "username is filled"); + is(form1.p.value, "pass1", "password is filled"); + + const form2 = setContentForTask(formTemplates.form2); + const autofillResult2 = await formAutofillResult(form2.id); + is(autofillResult2, "password_autocomplete_new_password", "form has not been filled due to password_autocomplete_new_password"); + + // 2nd form should not be filled + is(form2.uname.value, "", "username is empty"); + is(form2.password.value, "", "password is empty"); + + form2.uname.focus(); + await showACPopup(2, ["user1"]); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + + const autofillResult3 = await formAutofillResult(form2.id); + is(autofillResult3, "filled", "form has been filled"); + is(form2.uname.value, "user1", "username is filled"); + is(form2.password.value, "pass1", "password is filled"); + }); + + add_named_task("autofill autocomplete password no generation", async () => { + await setPreferencesForTask( + ["signon.generation.available", false], + ["signon.generation.enabled", false], + ); + await setStoredLoginsDuringTask([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = setContentForTask(formTemplates.form2); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "password_autocomplete_new_password", "form has not been filled due to password_autocomplete_new_password"); + + // form should not be filled + is(form.uname.value, "", "username is empty"); + is(form.password.value, "", "password is empty"); + + form.password.focus(); + // No generation option on username fields. + await showACPopup(2, ["user1"]); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + + await SimpleTest.promiseWaitForCondition(() => form.password.value == "pass1", "Check pw filled"); + is(form.uname.value, "", "username is empty"); + is(form.password.value, "pass1", "password is filled"); + + // No autocomplete results should appear for non-empty pw fields. + await noPopupByArrowDown(); + }); + + add_named_task("autofill autocomplete username no generation #2", async () => { + await setPreferencesForTask( + ["signon.generation.available", false], + ["signon.generation.enabled", false], + ); + await setStoredLoginsDuringTask([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = setContentForTask(formTemplates.form2); + const autofillResult1 = await formAutofillResult(form.id); + is(autofillResult1, "password_autocomplete_new_password", "form has not been filled due to password_autocomplete_new_password"); + + // form should not be filled + is(form.uname.value, "", "username is empty"); + is(form.password.value, "", "password is empty"); + + form.uname.focus(); + // No generation option on username fields. + await showACPopup(2, ["user1"]); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + + const autofillResult2 = await formAutofillResult(form.id); + is(autofillResult2, "filled", "form has been filled"); + is(form.uname.value, "user1", "username is filled"); + is(form.password.value, "pass1", "password is filled"); + }); + + add_named_task("autofill autocomplete passsword with generation", async () => { + await setPreferencesForTask( + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ); + + runInParent(clearGeneratedPasswords); + await setStoredLoginsDuringTask([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = setContentForTask(formTemplates.form2); + const formNumber = 2; + + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "password_autocomplete_new_password", "form has not been filled due to password_autocomplete_new_password"); + + form.reset(); + + // form should not be filled + is(form.uname.value, "", "username is empty"); + is(form.password.value, "", "password is empty"); + + form.password.focus(); + + await showACPopup(formNumber, [ + "user1", + "Use a Securely Generated Password", + ]); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await SimpleTest.promiseWaitForCondition(() => form.password.value == "pass1", "Check pw filled"); + is(form.uname.value, "", "username is empty"); + is(form.password.value, "pass1", "password is filled"); + + // No autocomplete results should appear for non-empty pw fields. + await noPopupByArrowDown(); + + info("Removing all logins to test auto-saving of generated passwords"); + await LoginManager.removeAllUserFacingLogins(); + + while (form.password.value) { + synthesizeKey("KEY_Backspace"); + } + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field"); + + info("This time select the generated password"); + await showACPopup(formNumber, [ + "Use a Securely Generated Password", + ]); + + synthesizeKey("KEY_ArrowDown"); + const storageAddPromise = promiseStorageChanged(["addLogin"]); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Before first fill of generated pw"); + synthesizeKey("KEY_Enter"); + + info("waiting for the password field to be filled with the generated password"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After first fill of generated pw"); + info("Wait for generated password to be added to storage"); + await storageAddPromise; + + let logins = await LoginManager.getAllLogins(); + const timePasswordChanged = logins[logins.length - 1].timePasswordChanged; + const time = dateAndTimeFormatter.format(new Date(timePasswordChanged)); + const LABEL_NO_USERNAME = "No username (" + time + ")"; + + const generatedPW = form.password.value; + is(generatedPW.length, GENERATED_PASSWORD_LENGTH, "Check generated password length"); + ok(generatedPW.match(GENERATED_PASSWORD_REGEX), "Check generated password format"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After fill"); + + info("Check field is masked upon blurring"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "After blur"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After shift-tab to focus again"); + // Remove selection for OS where the whole value is selected upon focus. + synthesizeKey("KEY_ArrowRight"); + + while (form.password.value) { + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, form.password.value); + synthesizeKey("KEY_Backspace"); + } + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field"); + + info("Blur the empty field to trigger a 'change' event"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after blanking"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after blanking"); + + info("Type a single character after blanking"); + synthesizeKey("@"); + + info("Blur the single-character field to trigger a 'change' event"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after backspacing"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after backspacing"); + synthesizeKey("KEY_Backspace"); // Blank the field again + + await showACPopup(formNumber, [ + LABEL_NO_USERNAME, + "Use a Securely Generated Password", + ]); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled"); + // Same generated password should be used, even despite the 'change' to @ earlier. + is(form.uname.value, "", "username is empty"); + is(form.password.value, generatedPW, "password is filled with generated password"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "Second fill of the generated pw"); + + logins = await LoginManager.getAllLogins(); + is(logins.length, 1, "Still 1 login after filling the generated password a 2nd time"); + is(logins[0].timePasswordChanged, timePasswordChanged, "Saved login wasn't changed"); + is(logins[0].password, generatedPW, "Password is the same"); + + info("filling the saved login to ensure the field is masked again"); + + while (form.password.value) { + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, form.password.value); + synthesizeKey("KEY_Backspace"); + } + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field again"); + + info("Blur the field to trigger a 'change' event again"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after blanking again"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after blanking again"); + // Remove selection for OS where the whole value is selected upon focus. + synthesizeKey("KEY_ArrowRight"); + + await showACPopup(formNumber, [ + LABEL_NO_USERNAME, + "Use a Securely Generated Password", + ]); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check saved generated pw filled"); + // Same generated password should be used but from storage + is(form.uname.value, "", "username is empty"); + is(form.password.value, generatedPW, "password is filled with generated password"); + // Passwords from storage should always be masked. + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after fill from storage"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after blur"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after shift-tab to focus again"); + }); + + add_named_task("autofill autocomplete passsword with generation #2", async () => { + await setPreferencesForTask( + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ); + + runInParent(clearGeneratedPasswords); + await setStoredLoginsDuringTask([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = setContentForTask(formTemplates.form3); + const formNumber = 3; + + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "filled", "form has been filled"); + + form.reset(); + + is(form.username.value, "", "username is empty"); + is(form.password.value, "", "password is empty"); + + form.password.focus(); + + await showACPopup(formNumber, [ + "user1", + "Use a Securely Generated Password", + ]); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await SimpleTest.promiseWaitForCondition(() => form.password.value == "pass1", "Check pw filled"); + is(form.username.value, "", "username is empty"); + is(form.password.value, "pass1", "password is filled"); + + // No autocomplete results should appear for non-empty pw fields. + await noPopupByArrowDown(); + + info("Removing all logins to test auto-saving of generated passwords"); + await LoginManager.removeAllUserFacingLogins(); + + while (form.password.value) { + synthesizeKey("KEY_Backspace"); + } + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field"); + + info("This time select the generated password"); + await showACPopup(formNumber, [ + "Use a Securely Generated Password", + ]); + + synthesizeKey("KEY_ArrowDown"); + const storageAddPromise = promiseStorageChanged(["addLogin"]); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Before first fill of generated pw"); + synthesizeKey("KEY_Enter"); + + info("waiting for the password field to be filled with the generated password"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After first fill of generated pw"); + info("Wait for generated password to be added to storage"); + await storageAddPromise; + + let logins = await LoginManager.getAllLogins(); + const timePasswordChanged = logins[logins.length - 1].timePasswordChanged; + const time = dateAndTimeFormatter.format(new Date(timePasswordChanged)); + const LABEL_NO_USERNAME = "No username (" + time + ")"; + + const generatedPW = form.password.value; + is(generatedPW.length, GENERATED_PASSWORD_LENGTH, "Check generated password length"); + ok(generatedPW.match(GENERATED_PASSWORD_REGEX), "Check generated password format"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After fill"); + + info("Check field is masked upon blurring"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "After blur"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "After shift-tab to focus again"); + // Remove selection for OS where the whole value is selected upon focus. + synthesizeKey("KEY_ArrowRight"); + + while (form.password.value) { + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, form.password.value); + synthesizeKey("KEY_Backspace"); + } + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field"); + + info("Blur the empty field to trigger a 'change' event"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after blanking"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after blanking"); + + info("Type a single character after blanking"); + synthesizeKey("@"); + + info("Blur the single-character field to trigger a 'change' event"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after backspacing"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after backspacing"); + synthesizeKey("KEY_Backspace"); // Blank the field again + + await showACPopup(formNumber, [ + LABEL_NO_USERNAME, + "Use a Securely Generated Password", + ]); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled"); + // Same generated password should be used, even despite the 'change' to @ earlier. + is(form.username.value, "", "username is empty"); + is(form.password.value, generatedPW, "password is filled with generated password"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "Second fill of the generated pw"); + + logins = await LoginManager.getAllLogins(); + is(logins.length, 1, "Still 1 login after filling the generated password a 2nd time"); + is(logins[0].timePasswordChanged, timePasswordChanged, "Saved login wasn't changed"); + is(logins[0].password, generatedPW, "Password is the same"); + + info("filling the saved login to ensure the field is masked again"); + + while (form.password.value) { + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, form.password.value); + synthesizeKey("KEY_Backspace"); + } + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blanked field again"); + + info("Blur the field to trigger a 'change' event again"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Blur after blanking again"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "Focus again after blanking again"); + // Remove selection for OS where the whole value is selected upon focus. + synthesizeKey("KEY_ArrowRight"); + + await showACPopup(formNumber, [ + LABEL_NO_USERNAME, + "Use a Securely Generated Password", + ]); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check saved generated pw filled"); + // Same generated password should be used but from storage + is(form.username.value, "", "username is empty"); + is(form.password.value, generatedPW, "password is filled with generated password"); + // Passwords from storage should always be masked. + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after fill from storage"); + synthesizeKey("KEY_Tab"); // blur + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after blur"); + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, true, "after shift-tab to focus again"); + }); + + add_named_task("autofill autocomplete password save login disabled", async () => { + await setPreferencesForTask( + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ); + await setStoredLoginsDuringTask([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = setContentForTask(formTemplates.form2); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "password_autocomplete_new_password", "form has not been filled due to password_autocomplete_new_password"); + + // form should not be filled + is(form.uname.value, "", "username is empty"); + is(form.password.value, "", "password is empty"); + + const formOrigin = new URL(document.documentURI).origin; + is(formOrigin, location.origin, "Expected form origin"); + + await LoginManager.setLoginSavingEnabled(location.origin, false); + + form.password.focus(); + // when login-saving is disabled for an origin, we expect no generated password row here + await showACPopup(2, ["user1"]); + + // close any open menu + synthesizeKey("KEY_Escape"); + await untilAutocompletePopupClosed(); + + await LoginManager.setLoginSavingEnabled(location.origin, true); + }); + + add_named_task("delete and reselect generated password", async () => { + await setPreferencesForTask( + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ); + await setStoredLoginsDuringTask([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = setContentForTask(formTemplates.form2); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "password_autocomplete_new_password", "form has not been filled due to password_autocomplete_new_password"); + + info("Removing all logins to test auto-saving of generated passwords"); + await LoginManager.removeAllUserFacingLogins(); + + // form should not be filled + is(form.uname.value, "", "username is empty"); + is(form.password.value, "", "password is empty"); + + async function showAndSelectACPopupItem(index) { + form.password.focus(); + if (form.password.value) { + form.password.select(); + synthesizeKey("KEY_Backspace"); + } + const autocompleteItems = await popupByArrowDown(); + if (index < 0) { + index = autocompleteItems.length + index; + } + for (let i = 0; i <= index; i++) { + synthesizeKey("KEY_ArrowDown"); + } + await TestUtils.waitForTick(); + return autocompleteItems[index]; + } + + let menuLabel, itemIndex, savedLogins; + + // fill the password field with the generated password via auto-complete menu + const addLoginPromise = promiseStorageChanged(["addLogin"]); + // select last-but-2 item - the one before the footer + menuLabel = await showAndSelectACPopupItem(-2); + is(menuLabel, "Use a Securely Generated Password", "Check item label"); + synthesizeKey("KEY_Enter"); + info("waiting for the password field to be filled with the generated password"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled"); + info("Wait for generated password to be added to storage"); + await addLoginPromise; + + savedLogins = await LoginManager.getAllLogins(); + is(savedLogins.length, 1, "Check saved logins count"); + + form.uname.focus(); + await TestUtils.waitForTick(); + + is(form.password.value.length, LoginTestUtils.generation.LENGTH, "Check password looks generated"); + const GENERATED_PASSWORD = form.password.value; + + info("clear the password field and delete the saved login using the AC menu") + const removeLoginPromise = promiseStorageChanged(["removeLogin"]); + + itemIndex = 0; + menuLabel = await showAndSelectACPopupItem(itemIndex); + ok(menuLabel.includes("No username"), "Check first item is the auto-saved login"); + // Send delete to remove the auto-saved login from storage + // On OS X, shift-backspace and shift-delete work, just delete does not. + // On Win/Linux, shift-backspace does not work, delete and shift-delete do. + synthesizeKey("KEY_Delete", {shiftKey: true}); + await removeLoginPromise; + + form.uname.focus(); + await TestUtils.waitForTick(); + + savedLogins = await LoginManager.getAllLogins(); + is(savedLogins.length, 0, "Check saved logins count"); + + info("Re-fill with the generated password"); + // select last-but-2 item - the one before the footer + menuLabel = await showAndSelectACPopupItem(-2); + is(menuLabel, "Use a Securely Generated Password", "Check item label"); + synthesizeKey("KEY_Enter"); + info("waiting for the password field to be filled with the generated password"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled"); + + form.uname.focus(); + await TestUtils.waitForTick(); + is(form.password.value, GENERATED_PASSWORD, "Generated password has not changed"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html new file mode 100644 index 0000000000..54cb450815 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html @@ -0,0 +1,380 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>Test filling generated passwords into confirm password fields</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="pwmgr_common.js"></script> + <script src="../../../satchel/test/satchel_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Login Manager test: filling generated passwords into confirm password fields + +<template id="form1-template"> + <form id="form1" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="password" name="pwordNext"> + <button type="submit">Submit</button> + </form> +</template> + +<template id="form2-template"> + <form id="form2" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="password" name="pwordNext" value="initial value"> + <button type="submit">Submit</button> + </form> +</template> + +<template id="form3-template"> + <form id="form3" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="password" name="pwordNext" readonly> + <button type="submit">Submit</button> + </form> +</template> + +<template id="form4-template"> + <form id="form4" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="password" name="pwordNext" disabled> + <button type="submit">Submit</button> + </form> +</template> + +<template id="form5-template"> + <form id="form5" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="password" name="pwordBetween"> + <input type="password" name="pwordNext" autocomplete="new-password"> + <button type="submit">Submit</button> + </form> +</template> + +<template id="form6-template"> + <form id="form6" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="password" name="pwordNext"> + <input type="password" name="pwordAfter" autocomplete="new-password" disabled> + <button type="submit">Submit</button> + </form> +</template> + +<template id="form7-template"> + <form id="form7" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="password" name="junk0"> + <input type="password" name="junk1"> + <input type="password" name="junk2"> + <input type="password" name="junk3"> + <input type="password" name="junk4"> + <input type="password" name="pwordNext" autocomplete="new-password"> + <button type="submit">Submit</button> + </form> +</template> + +<template id="form8-template"> + <form id="form8" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="password" name="junk0" disabled> + <input type="password" name="junk1" disabled> + <input type="password" name="junk2" disabled> + <input type="password" name="junk3" disabled> + <input type="password" name="junk4" disabled> + <input type="password" name="pwordNext" autocomplete="new-password"> + <button type="submit">Submit</button> + </form> +</template> + +<template id="form9-template"> + <form id="form9" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="hidden" name="junk0"> + <input type="hidden" name="junk1"> + <input type="hidden" name="junk2"> + <input type="hidden" name="junk3"> + <input type="hidden" name="junk4"> + <input type="password" name="pwordNext" autocomplete="new-password"> + <button type="submit">Submit</button> + </form> +</template> + +<template id="form10-template"> + <form id="form10" action="https://example.com"> + <input type="text" name="uname"> + <input type="password" name="pword" autocomplete="new-password"> + <input type="password" name="pwordNext" autocomplete="new-password"> + <input type="password" name="pwordExtra" autocomplete="new-password"> + <button type="submit">Submit</button> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const formTemplates = { + form1: document.getElementById("form1-template"), + form2: document.getElementById("form2-template"), + form3: document.getElementById("form3-template"), + form4: document.getElementById("form4-template"), + form5: document.getElementById("form5-template"), + form6: document.getElementById("form6-template"), + form7: document.getElementById("form7-template"), + form8: document.getElementById("form8-template"), + form9: document.getElementById("form9-template"), + form10: document.getElementById("form10-template"), + }; + + const setupScript = runInParent(function parentTestSetup() { + const { LoginTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/LoginTestUtils.sys.mjs" + ); + + addMessageListener( + "resetLoginsAndGeneratedPasswords", () => { + LoginTestUtils.clearData(); + LoginTestUtils.resetGeneratedPasswordsCache(); + } + ); + }); + + function resetLoginsAndGeneratedPasswords() { + return setupScript.sendAsyncMessage("resetLoginsAndGeneratedPasswords"); + } + + async function triggerPasswordGeneration(form) { + await openPopupOn(form.pword); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await SimpleTest.promiseWaitForCondition(() => !!form.pword.value, "Wait for generated password to get filled"); + } + + add_named_task("autocomplete menu contains option to generate password", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + + const { items } = await openPopupOn(form.pword); + + checkAutoCompleteResults(items, [ + "Use a Securely Generated Password" + ], location.host, "Check all rows are correct"); + }); + + add_named_task("username field highlight", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + ok(!form.uname.matches(":autofill"), "Highlight was not applied to the username field"); + }); + + add_named_task("password field highlight", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + ok(form.pword.matches(":autofill"), "Highlight was applied to the password field"); + }); + + add_named_task("username field is left untouched", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + is(form.uname.value, "", "Value is still empty") + }); + + add_named_task("generated password looks like a generated password", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + const generatedPassword = form.pword.value; + is(generatedPassword.length, GENERATED_PASSWORD_LENGTH, "Generated password length matches"); + ok(generatedPassword.match(GENERATED_PASSWORD_REGEX), "Generated password format matches"); + }); + + add_named_task("password confirmation also gets filled with the generated password", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password"); + }); + + add_named_task("password field is not masked initially after password generation", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, false, "password field is not masked after password generation"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pwordNext, true, "password confirmation field is masked after user input"); + }); + + add_named_task("password field is masked after user input", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + form.pwordNext.focus(); + form.pwordNext.select(); + synthesizeKey("KEY_Backspace"); + synthesizeKey("a"); + form.pwordNext.blur(); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pword, true, "password field is masked after user input"); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pwordNext, true, "password confirmation field is also masked after user input"); + }); + + add_named_task("password field highlight is cleared after user input", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + form.pwordNext.focus(); + form.pwordNext.select(); + synthesizeKey("KEY_Backspace"); + synthesizeKey("a"); + form.pwordNext.blur(); + await SimpleTest.promiseWaitForCondition(() => !form.pwordNext.matches(":autofill"), "Highlight was cleared"); + }); + + add_named_task("generated password can be changed", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + const generatedPassword = form.pword.value; + form.pword.focus(); + synthesizeKey("KEY_End"); + synthesizeKey("@"); + is(form.pword.value, `${generatedPassword}@`, "Value of the password field changed"); + }); + + add_named_task("password confirmation field does not receive changes from password field", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + const generatedPassword = form.pword.value; + + // changing the password field value should result in a message sent to the parent process + const messageSentPromise = getPasswordEditedMessage(); + + form.pword.focus(); + synthesizeKey("KEY_End"); + synthesizeKey("@"); + + // bluring results in a "change" event + form.pword.blur(); + await messageSentPromise; + + is(form.pwordNext.value, generatedPassword, "Value of the confirm field still holds the original generated password"); + ok(form.pwordNext.matches(":autofill"), "Highlight is still applied to password confirmation field"); + }); + + add_named_task("password confirmation field behaves like a normal password field once changed", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + await triggerPasswordGeneration(form); + + form.pwordNext.focus(); + form.pwordNext.select(); + synthesizeKey("KEY_Backspace"); + + // verify the focused confirm field now masks its input like a normal, + // non-generated password field after being emptied + form.pwordNext.focus(); + synthesizeKey("a"); + form.pwordNext.blur(); + + LOGIN_FIELD_UTILS.checkPasswordMasked(form.pwordNext, true, "password confirmation field is masked"); + await SimpleTest.promiseWaitForCondition(() => !form.pwordNext.matches(":autofill"), "highlight was cleared"); + }); + + add_named_task("password confirmation also gets filled with the generated password, even if it has been changed to be of type text", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + form.pwordNext.type = "text"; + await triggerPasswordGeneration(form); + is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password"); + }); + + add_named_task("password confirmation does not get filled with the generated password if it is not empty", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form2); + await triggerPasswordGeneration(form); + is(form.pwordNext.value, "initial value", "Value of the confirm field has been filled with generated password"); + }); + + add_named_task("password confirmation does not get filled with the generated password if it has been edited", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form1); + form.pwordNext.focus() + sendString("edited value"); + await triggerPasswordGeneration(form); + is(form.pwordNext.value, "edited value", "Value of the confirm field has been filled with generated password"); + }); + + add_named_task("password confirmation does not get filled with the generated password if its readonly", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form3); + await triggerPasswordGeneration(form); + is(form.pwordNext.value, "", "Value of the confirm field has been filled with generated password"); + }); + + add_named_task("password confirmation does not get filled with the generated password if its disabled", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form4); + await triggerPasswordGeneration(form); + is(form.pwordNext.value, "", "Value of the confirm field has been filled with generated password"); + }); + + add_named_task("password confirmation matching autocomplete info gets filled with the generated password", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form5); + await triggerPasswordGeneration(form); + is(form.pwordBetween.value, "", "Value of the between field has not been filled"); + is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password"); + }); + + add_named_task("password confirmation matching autocomplete info gets ignored if its disabled, even if has autocomplete info", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form6); + await triggerPasswordGeneration(form); + is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password"); + is(form.pwordAfter.value, "", "Value of the disabled confirmation field has not been filled"); + }); + + add_named_task("password confirmation matching autocomplete info gets ignored there are too many fields in between, even if has autocomplete info", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form7); + await triggerPasswordGeneration(form); + is(form.pwordNext.value, "", "Value of the confirm field has not been filled"); + }); + + add_named_task("password confirmation matching autocomplete info gets ignored there are too many disabled fields in between, even if has autocomplete info", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form8); + await triggerPasswordGeneration(form); + is(form.pwordNext.value, "", "Value of the confirm field has not been filled"); + }); + + add_named_task("password confirmation matching autocomplete info gets filled even if there are many hidden fields in between", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form9); + await triggerPasswordGeneration(form); + is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password"); + }); + + add_named_task("do not fill third password field after the confirm-password field", async () => { + await resetLoginsAndGeneratedPasswords(); + const form = setContentForTask(formTemplates.form10); + await triggerPasswordGeneration(form); + is(form.pwordExtra.value, "", "Value of the additional confirm field has not been filled"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_telemetry.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_telemetry.html new file mode 100644 index 0000000000..4bbba09483 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_telemetry.html @@ -0,0 +1,309 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test telemetry on autofill and autocomplete on autocomplete=new-password fields</title> + <!-- This test assumes that telemetry events are not cleared after the `setup` task. --> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="pwmgr_common.js"></script> + <script src="../../../satchel/test/satchel_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Login Manager test: telemetry events during autofill with password generation on `autocomplete=new-password` fields. + +<template id="form1-template"> + <form id="form1" action="https://autofill"> + <input type="text" name="uname"> + <input type="password" name="p"> + <button type="submit" name="submit">Submit</button> + </form> +</template> + +<template id="form2-template"> + <form id="form2" action="https://autofill"> + <input type="text" name="uname"> + <input type="password" name="password" autocomplete="new-password"> + <button type="submit" name="submit">Submit</button> + </form> +</template> + +<template id="form3-template"> + <form id="form3" action="https://autofill"> + <input type="text" name="username"> + <label>New password<input type="password" name="password"></label> + <button type="submit" name="submit">Submit</button> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const { TestUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" + ); + + const formTemplates = { + form1: document.getElementById("form1-template"), + form2: document.getElementById("form2-template"), + form3: document.getElementById("form3-template"), + }; + + const dateAndTimeFormatter = new SpecialPowers.Services.intl.DateTimeFormat(undefined, { + dateStyle: "medium", + }); + + const TelemetryFilterPropsUsed = Object.freeze({ + category: "pwmgr", + method: "autocomplete_field", + object: "generatedpassword", + }); + + const TelemetryFilterPropsShown = Object.freeze({ + category: "pwmgr", + method: "autocomplete_shown", + object: "generatedpassword", + }); + + async function waitForTelemetryEventsCondition(cond, options = {}, + errorMsg = "waitForTelemetryEventsCondition timed out", maxTries = 200) { + return TestUtils.waitForCondition(async () => { + let events = await getTelemetryEvents(options); + let result; + try { + result = cond(events); + info("waitForTelemetryEventsCondition, result: " + result); + } catch (e) { + info("waitForTelemetryEventsCondition caught exception, got events: " + JSON.stringify(events)); + ok(false, `${e}\n${e.stack}`); + } + return result ? events : null; + }, errorMsg, maxTries); + } + + async function showACPopup(formNumber, expectedACLabels) { + const autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, expectedACLabels, + window.location.host, "Check all rows are correct"); + } + + async function checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents) { + info(`showed generated password option, check there are now ${expectedPWGenTelemetryEvents} generatedpassword telemetry events`); + await waitForTelemetryEventsCondition(events => { + return events.length == expectedPWGenTelemetryEvents; + }, { process: "parent", filterProps: TelemetryFilterPropsShown }, `Wait for there to be ${expectedPWGenTelemetryEvents} shown telemetry events`); + } + + async function checkTelemetryEventsPWGenUsed(expectedPWGenTelemetryEvents) { + info("filled generated password again, ensure we don't record another generatedpassword autocomplete telemetry event"); + let telemetryEvents; + try { + telemetryEvents = await waitForTelemetryEventsCondition(events => events.length == expectedPWGenTelemetryEvents + 1, + { process: "parent", filterProps: TelemetryFilterPropsUsed }, + `Wait for there to be ${expectedPWGenTelemetryEvents + 1} used events`, 50); + } catch (ex) {} + ok(!telemetryEvents, `Expected to timeout waiting for there to be ${expectedPWGenTelemetryEvents + 1} events`); + } + + function clearGeneratedPasswords() { + const { LoginManagerParent } = ChromeUtils.importESModule( + "resource://gre/modules/LoginManagerParent.sys.mjs" + ); + if (LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin()) { + LoginManagerParent.getGeneratedPasswordsByPrincipalOrigin().clear(); + } + } + + add_setup(async () => { + let useEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsUsed, clear: true }); + is(useEvents.length, 0, "Expect 0 use events"); + let showEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsShown, clear: true }); + is(showEvents.length, 0, "Expect 0 show events"); + let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }); + is(acEvents.length, 0, "Expect 0 autocomplete events"); + }); + + add_named_task("autofill autocomplete username no generation", async () => { + await setPreferencesForTask( + ["signon.generation.available", false], + ["signon.generation.enabled", false], + ); + await setStoredLoginsDuringTask([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = setContentForTask(formTemplates.form2); + const autofillResult = await formAutofillResult(form.id); + is(autofillResult, "password_autocomplete_new_password", "form has not been filled due to password_autocomplete_new_password"); + + form.uname.focus(); + + const [acEvent] = await waitForTelemetryEventsCondition(events => { + return events.length == 1; + }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`); + checkACTelemetryEvent(acEvent, form.uname, { + "hadPrevious": "0", + "login": "1", + "loginsFooter": "1" + }); + }); + + add_named_task("filling out two forms one after the other", async () => { + await setPreferencesForTask( + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ); + + const formsToTest = [ + { + num: 2, + template: formTemplates.form2 + }, + { + num: 3, + template: formTemplates.form3 + } + ]; + + // Bug 1616356 and Bug 1548878: Recorded once per origin + let expectedPWGenTelemetryEvents = 0; + // Bug 1619498: Recorded once every time the autocomplete popup is shown + let expectedACShownTelemetryEvents = 0; + + for (const { num: formNumber, template } of formsToTest) { + runInParent(clearGeneratedPasswords); + await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = setContentForTask(template); + await promiseFormsProcessedInSameProcess(); + form.reset(); + + form.password.focus(); + + await showACPopup(formNumber, [ + "user1", + "Use a Securely Generated Password", + ]); + expectedPWGenTelemetryEvents++; + expectedACShownTelemetryEvents++; + + await checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents); + let acEvents = await waitForTelemetryEventsCondition(events => { + return events.length == expectedACShownTelemetryEvents; + }, { process: "parent", filterProps: TelemetryFilterPropsAC }, `Wait for there to be ${expectedACShownTelemetryEvents} autocomplete telemetry event(s)`); + checkACTelemetryEvent(acEvents[expectedACShownTelemetryEvents - 1], form.password, { + "generatedPasswo": "1", + "hadPrevious": "0", + "login": "1", + "loginsFooter": "1" + }); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + // Can't use promiseFormsProcessedInSameProcess() when autocomplete fills the field directly. + await SimpleTest.promiseWaitForCondition(() => form.password.value == "pass1", "Check pw filled"); + + // No autocomplete results should appear for non-empty pw fields. + await noPopupByArrowDown(); + + info("Removing all logins to test auto-saving of generated passwords"); + await LoginManager.removeAllUserFacingLogins(); + + while (form.password.value) { + synthesizeKey("KEY_Backspace"); + } + + info("This time select the generated password"); + await showACPopup(formNumber, [ + "Use a Securely Generated Password", + ]); + expectedACShownTelemetryEvents++; + + await checkTelemetryEventsPWGenShown(expectedPWGenTelemetryEvents); + acEvents = await waitForTelemetryEventsCondition(events => { + return events.length == expectedACShownTelemetryEvents; + }, { process: "parent", filterProps: TelemetryFilterPropsAC }, `Wait for there to be ${expectedACShownTelemetryEvents} autocomplete telemetry event(s)`); + checkACTelemetryEvent(acEvents[expectedACShownTelemetryEvents - 1], form.password, { + "generatedPasswo": "1", + "hadPrevious": "0", + "loginsFooter": "1" + }); + + synthesizeKey("KEY_ArrowDown"); + let storageAddPromise = promiseStorageChanged(["addLogin"]); + synthesizeKey("KEY_Enter"); + + info("waiting for the password field to be filled with the generated password"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled"); + info("Wait for generated password to be added to storage"); + await storageAddPromise; + + let logins = await LoginManager.getAllLogins(); + let timePasswordChanged = logins[logins.length - 1].timePasswordChanged; + let time = dateAndTimeFormatter.format(new Date(timePasswordChanged)); + const LABEL_NO_USERNAME = "No username (" + time + ")"; + + info("Check field is masked upon blurring"); + synthesizeKey("KEY_Tab"); // blur + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + // Remove selection for OS where the whole value is selected upon focus. + synthesizeKey("KEY_ArrowRight"); + + while (form.password.value) { + synthesizeKey("KEY_Backspace"); + } + + info("Blur the empty field to trigger a 'change' event"); + synthesizeKey("KEY_Tab"); // blur + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + + info("Type a single character after blanking"); + synthesizeKey("@"); + + info("Blur the single-character field to trigger a 'change' event"); + synthesizeKey("KEY_Tab"); // blur + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + synthesizeKey("KEY_Backspace"); // Blank the field again + + await showACPopup(formNumber, [ + LABEL_NO_USERNAME, + "Use a Securely Generated Password", + ]); + expectedACShownTelemetryEvents++; + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check generated pw filled"); + + await checkTelemetryEventsPWGenUsed(expectedPWGenTelemetryEvents); + + info("filling the saved login to ensure the field is masked again"); + + while (form.password.value) { + synthesizeKey("KEY_Backspace"); + } + + info("Blur the field to trigger a 'change' event again"); + synthesizeKey("KEY_Tab"); // blur + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus again + // Remove selection for OS where the whole value is selected upon focus. + synthesizeKey("KEY_ArrowRight"); + + await showACPopup(formNumber, [ + LABEL_NO_USERNAME, + "Use a Securely Generated Password", + ]); + expectedACShownTelemetryEvents++; + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await SimpleTest.promiseWaitForCondition(() => !!form.password.value, "Check saved generated pw filled"); + synthesizeKey("KEY_Tab"); // blur + synthesizeKey("KEY_Tab", { shiftKey: true }); // focus + } + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_open.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_open.html new file mode 100644 index 0000000000..e792aa19af --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_open.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test password field autocomplete footer with and without logins</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: Test password field autocomplete footer with and without logins **/ + +add_task(async function test_no_autofill() { + await setStoredLoginsAsync( + [location.origin, "", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = createLoginForm(); + await promiseFormsProcessedInSameProcess(); + + // Make sure initial form is empty as autofill shouldn't happen in the sandboxed frame. + checkLoginForm(form.uname, "", form.pword, ""); + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is initially closed"); +}); + +add_task(async function test_two_logins() { + await setStoredLoginsAsync( + [location.origin, "", null, "user-1", "pass-1", "uname", "pword"], + [location.origin, "", null, "user-2", "pass-2", "uname", "pword"] + ); + const form = createLoginForm(); + await promiseFormsProcessedInSameProcess(); + + await popupBy(() => form.uname.focus()); + + // popup on the password field should open upon focus + let results = await popupBy(() => synthesizeKey("KEY_Tab")); + + let popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + + let expectedMenuItems = [ + "user-1", + "user-2", + ]; + checkAutoCompleteResults(results, expectedMenuItems, window.location.host, "Check all menuitems are displayed correctly."); + + checkLoginForm(form.uname, "", form.pword, ""); +}); + +add_task(async function test_zero_logins() { + // no logins stored + await setStoredLoginsAsync(); + const form = createLoginForm(); + await promiseFormsProcessedInSameProcess(); + + form.uname.focus(); + + let shownPromise = popupBy().then(() => ok(false, "Should not have shown")); + // Popup on the password field should NOT automatically open upon focus when there are no saved logins. + synthesizeKey("KEY_Tab"); // focus the password field + SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popup to show"); + let autocompleteItems = await Promise.race([ + shownPromise, + new Promise(resolve => setTimeout(resolve, 2000)), // Wait 2s for the popup to appear + ]); + + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is still closed"); + + checkLoginForm(form.uname, "", form.pword, ""); + info("arrow down should still open the popup"); + autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, [], window.location.host, "Check only footer is displayed."); + checkLoginForm(form.uname, "", form.pword, ""); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_sandboxed.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_sandboxed.html new file mode 100644 index 0000000000..229791109a --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_sandboxed.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test form field autocomplete in sandboxed documents (null principal)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"> + <iframe id="sandboxed" + sandbox="" + src="form_basic.html"></iframe> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: form field autocomplete in sandboxed documents (null principal) **/ + +let sandboxed = document.getElementById("sandboxed"); +let uname; +let pword; +let hostname; + +add_setup(async () => { + await setStoredLoginsAsync( + [window.location.origin, "", null, "tempuser1", "temppass1", "uname", "pword"] + ); + let frameWindow = SpecialPowers.wrap(sandboxed).contentWindow; + // Can't use SimpleTest.promiseFocus as it doesn't work with the sandbox. + await SimpleTest.promiseWaitForCondition(() => { + return frameWindow.document.readyState == "complete" && frameWindow.location.href != "about:blank"; + }, "Check frame is loaded"); + let frameDoc = SpecialPowers.wrap(sandboxed).contentDocument; + uname = frameDoc.getElementById("form-basic-username"); + pword = frameDoc.getElementById("form-basic-password"); + hostname = frameDoc.documentURIObject.host; +}); + +add_task(async function test_no_autofill() { + // Make sure initial form is empty as autofill shouldn't happen in the sandboxed frame. + checkLoginForm(uname, "", pword, ""); + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is initially closed"); +}); + +add_task(async function test_autocomplete_warning_no_logins() { + const { items } = await openPopupOn(pword); + + let popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + + let expectedMenuItems = [ + "This connection is not secure. Logins entered here could be compromised. Learn More", + ]; + checkAutoCompleteResults(items, expectedMenuItems, hostname, "Check all menuitems are displayed correctly."); + + checkLoginForm(uname, "", pword, ""); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_tab_between_fields.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_tab_between_fields.html new file mode 100644 index 0000000000..4a0aaff0b3 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_tab_between_fields.html @@ -0,0 +1,167 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autocomplete behavior when tabbing between form fields</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +const { TestUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const availableLogins = { + "exampleUser1": [location.origin, "https://autofill", null, "user1", "pass1", "uname", "pword"], + "subdomainUser1": ["https://sub." + location.host, "https://autofill", null, "user1", "pass1", "uname", "pword"], + "emptyUsername": [location.origin, "https://autofill", null, "", "pass2", "uname", "pword"], +} + +const tests = [ + { + name: "single_login_exact_origin_no_inputs", + logins: ["exampleUser1"], + expectedAutofillUsername: "user1", + expectedAutofillPassword: "pass1", + expectedACLabels: ["user1"], + typeUsername: null, + expectedTabbedUsername: "", + expectedTabbedPassword: "", + }, + { + name: "single_login_exact_origin_initial_letter", + logins: ["exampleUser1"], + expectedAutofillUsername: "user1", + expectedAutofillPassword: "pass1", + expectedACLabels: ["user1"], + typeUsername: "u", + expectedTabbedUsername: "u", + expectedTabbedPassword: "", + }, + { + name: "single_login_exact_origin_type_username", + logins: ["exampleUser1"], + expectedAutofillUsername: "user1", + expectedAutofillPassword: "pass1", + expectedACLabels: ["user1"], + typeUsername: "user1", + expectedTabbedUsername: "user1", + expectedTabbedPassword: "pass1", + }, + { + name: "single_login_subdomain_no_inputs", + logins: ["subdomainUser1"], + expectedAutofillUsername: "", + expectedAutofillPassword: "", + expectedACLabels: ["user1"], + typeUsername: null, + expectedTabbedUsername: "", + expectedTabbedPassword: "", + }, + { + name: "single_login_subdomain_type_username", + logins: ["subdomainUser1"], + expectedAutofillUsername: "", + expectedAutofillPassword: "", + expectedACLabels: ["user1"], + typeUsername: "user1", + expectedTabbedUsername: "user1", + expectedTabbedPassword: "", + }, + { + name: "two_logins_one_with_empty_username", + logins: ["exampleUser1", "emptyUsername"], + expectedAutofillUsername: "user1", + expectedAutofillPassword: "pass1", + expectedACLabels: ["user1"], + typeUsername: "", + expectedTabbedUsername: "", + expectedTabbedPassword: "", + }, +]; + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({"set": [["signon.includeOtherSubdomainsInLookup", true]]}); +}); + +async function testResultOfTabInteractions(testData) { + const logins = testData.logins.map(name => availableLogins[name]); + await setStoredLoginsAsync(...logins); + + const form = createLoginForm({ + action: "https://autofill" + }); + await promiseFormsProcessedInSameProcess(); + + await SimpleTest.promiseFocus(window); + + // check autofill results + checkForm(1, testData.expectedAutofillUsername, testData.expectedAutofillPassword); + + SpecialPowers.wrap(form.pword).setUserInput(""); + SpecialPowers.wrap(form.uname).setUserInput(""); + + info("Placing focus in the password field"); + form.pword.focus(); + await synthesizeKey("KEY_Tab", { shiftKey: true }); // blur pw, focus un + + // moving focus shouldn't change anything + await ensureLoginFormStaysFilledWith(form.uname, "", form.pword, ""); + + info("waiting for AC results"); + const results = await popupByArrowDown(); + info("checking results"); + checkAutoCompleteResults(results, testData.expectedACLabels, + window.location.host, "Check all rows are correct"); + + if (testData.typeUsername) { + await sendString(testData.typeUsername); + } + + // don't select anything from the AC menu + await synthesizeKey("KEY_Escape"); + await TestUtils.waitForCondition(async () => { + let popupState = await getPopupState(); + return !popupState.open; + }, "AutoComplete popup should have closed"); + + await synthesizeKey("KEY_Tab"); + + // wait until username and password are automatically filled in with the + // expected values... + await TestUtils.waitForCondition(() => { + return form.uname.value === testData.expectedTabbedUsername & form.pword.value === testData.expectedTabbedPassword; + }, "Username and password field should be filled"); + + // ...and if the value is not different from the original value in the form, + // make sure that the form keeps its values + if (testData.expectedTabbedPassword === "") { + await ensureLoginFormStaysFilledWith(form.uname, testData.expectedTabbedUsername, form.pword, testData.expectedTabbedPassword); + } + + ok(form.pword.matches("input:focus"), "pword field is focused"); +} + +for (const testData of tests) { + const tmp = { + async [testData.name]() { + await testResultOfTabInteractions(testData); + }, + }; + add_task(tmp[testData.name]); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_autocomplete_types.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_autocomplete_types.html new file mode 100644 index 0000000000..c44d5a25ef --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_autocomplete_types.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofilling with autocomplete types (username, off, cc-type, etc.)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Test autofilling with autocomplete types (username, off, cc-type, etc.) + +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/* + Test for Login Manager: Skip over inappropriate autcomplete types when finding username field + */ + +const win = window.open("about:blank"); +const loadPromise = loadFormIntoWindow(location.origin, ` + <form id="form0" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="text" autocomplete=""> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form1" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="text" autocomplete="username"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form2" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="text" autocomplete="off" name="acfield"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form3" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="text" autocomplete="on" name="acfield"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form4" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="text" autocomplete="nosuchtype" name="acfield"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form5" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="text" autocomplete="email" name="acfield"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form6" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="text" autocomplete="tel" name="acfield"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form7" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="text" autocomplete="tel-national" name="acfield"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <!-- Begin forms where the first field is skipped for the username --> + + <form id="form101" action="https://autocomplete"> + <input type="text" name="uname"> + <input type="text" autocomplete="cc-number" name="acfield"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form>`, win, 8); + +add_setup(async () => { + await setStoredLoginsAsync( + [window.location.origin, "https://autocomplete", null, "testuser@example.com", "testpass1", "", ""] + ); + SimpleTest.registerCleanupFunction(() => win.close()); +}); + +/* Tests for autofill of single-user forms with various autocomplete types */ +add_task(async function test_autofill_autocomplete_types() { + await loadPromise; + await checkLoginFormInFrameWithElementValues(win, 0, null, "testuser@example.com", "testpass1"); + await checkLoginFormInFrameWithElementValues(win, 1, null, "testuser@example.com", "testpass1"); + await checkLoginFormInFrameWithElementValues(win, 2, null, "testuser@example.com", "testpass1"); + await checkLoginFormInFrameWithElementValues(win, 3, null, "testuser@example.com", "testpass1"); + await checkLoginFormInFrameWithElementValues(win, 4, null, "testuser@example.com", "testpass1"); + await checkLoginFormInFrameWithElementValues(win, 5, null, "testuser@example.com", "testpass1"); + await checkLoginFormInFrameWithElementValues(win, 6, null, "testuser@example.com", "testpass1"); + await checkLoginFormInFrameWithElementValues(win, 7, null, "testuser@example.com", "testpass1"); + await checkLoginFormInFrameWithElementValues(win, 101, "testuser@example.com", null, "testpass1"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_formActionOrigin.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_formActionOrigin.html new file mode 100644 index 0000000000..1ea43f6a8f --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_formActionOrigin.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofill on an HTTPS page using upgraded HTTP logins with different formActionOrigin</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html"; + +const chromeScript = runChecksAfterCommonInit(false); + +let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1", + SpecialPowers.Ci.nsILoginInfo, + "init"); +</script> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +let origin = window.location.origin; +let suborigin = "http://sub." + window.location.host; + +let win = window.open("about:blank"); +SimpleTest.registerCleanupFunction(() => win.close()); + +async function prepareLoginsAndProcessForm(url, logins = []) { + await LoginManager.removeAllUserFacingLogins(); + + let dates = Date.now(); + for (let login of logins) { + SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo); + // Force all dates to be the same so they don't affect things like deduping. + login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates; + await LoginManager.addLoginAsync(login); + } + + let processedPromise = promiseFormsProcessed(); + win.location = url; + await processedPromise; +} + +add_task(async function test_formActionOrigin_wildcard_should_autofill() { + await prepareLoginsAndProcessForm(origin + MISSING_ACTION_PATH, [ + new nsLoginInfo(origin, "", null, + "name2", "pass2", "uname", "pword"), + ]); + + await checkLoginFormInFrame(win, "form-basic-username", "name2", "form-basic-password", "pass2"); +}); + +add_task(async function test_formActionOrigin_different_shouldnt_autofill() { + await prepareLoginsAndProcessForm(origin + MISSING_ACTION_PATH, [ + new nsLoginInfo(origin, "https://example.net", null, + "name2", "pass2", "uname", "pword"), + ]); + + + await checkLoginFormInFrame(win, "form-basic-username", "", "form-basic-password", ""); +}); + +add_task(async function test_formActionOrigin_subdomain_should_autofill() { + await prepareLoginsAndProcessForm(origin + MISSING_ACTION_PATH, [ + new nsLoginInfo(origin, suborigin, null, + "name2", "pass2", "uname", "pword"), + ]); + + await checkLoginFormInFrame(win, "form-basic-username", "name2", "form-basic-password", "pass2"); +}); + +add_task(async function test_origin_subdomain_should_not_autofill() { + await prepareLoginsAndProcessForm(origin + MISSING_ACTION_PATH, [ + new nsLoginInfo(suborigin, origin, null, + "name2", "pass2", "uname", "pword"), + ]); + + await checkLoginFormInFrame(win, "form-basic-username", "", "form-basic-password", ""); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_subdomain.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_subdomain.html new file mode 100644 index 0000000000..66c6ab3536 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_different_subdomain.html @@ -0,0 +1,150 @@ +xcod<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofill on an HTTPS page using logins with different eTLD+1</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html"; + +const chromeScript = runChecksAfterCommonInit(false); + +let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1", + SpecialPowers.Ci.nsILoginInfo, + "init"); +</script> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"> + <iframe></iframe> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +let iframe = SpecialPowers.wrap(document.getElementsByTagName("iframe")[0]); +let win = window.open("about:blank"); +SimpleTest.registerCleanupFunction(() => win.close()); + +let origin = window.location.origin; +let otherOrigin = "https://foobar." + window.location.host; +let oldOrigin = "https://old." + window.location.host; + +async function checkWindowLoginForm(expectedUsername, expectedPassword) { + return SpecialPowers.spawn(win, [expectedUsername, expectedPassword], function(un, pw) { + let doc = this.content.document; + Assert.equal(doc.querySelector("#form-basic-username").value, un, "Check username value"); + Assert.equal(doc.querySelector("#form-basic-password").value, pw, "Check password value"); + }); +} + +async function prepareLogins(logins = []) { + await LoginManager.removeAllUserFacingLogins(); + + let dates = Date.now(); + for (let login of logins) { + SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo); + // Force all dates to be the same so they don't affect things like deduping. + login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates; + await LoginManager.addLoginAsync(login); + } +} + +async function formReadyInFrame(url) { + let processedPromise = promiseFormsProcessed(); + iframe.src = url; + return processedPromise; +} + +async function formReadyInWindow(url) { + let processedPromise = promiseFormsProcessedInSameProcess(); + win.location = url; + return processedPromise; +} + +add_task(async function test_login_with_different_subdomain_shouldnt_autofill_wildcard_formActionOrigin() { + await prepareLogins([ + new nsLoginInfo(otherOrigin, "", null, + "name2", "pass2", "uname", "pword"), + ]); + await formReadyInWindow(origin + MISSING_ACTION_PATH); + + await checkWindowLoginForm("", ""); +}); + +add_task(async function test_login_with_different_subdomain_shouldnt_autofill_same_domain_formActionOrigin() { + await prepareLogins([ + new nsLoginInfo(otherOrigin, origin, null, + "name2", "pass2", "uname", "pword"), + ]); + await formReadyInWindow(origin + MISSING_ACTION_PATH); + + await checkWindowLoginForm("", ""); +}); + +add_task(async function test_matching_logins_with_different_subdomain_and_matching_domain_should_autofill() { + await prepareLogins([ + new nsLoginInfo(origin, origin, null, + "name2", "pass2", "uname", "pword"), + new nsLoginInfo(oldOrigin, origin, null, + "name2", "pass2", "uname", "pword"), + ]); + await formReadyInWindow(origin + MISSING_ACTION_PATH); + + await checkWindowLoginForm("name2", "pass2"); +}); + +add_task(async function test_login_with_different_subdomain_shouldnt_autofill_different_subdomain_formActionOrigin() { + await prepareLogins([ + new nsLoginInfo(otherOrigin, otherOrigin, null, + "name2", "pass2", "uname", "pword"), + ]); + await formReadyInWindow(origin + MISSING_ACTION_PATH); + + await checkWindowLoginForm("", ""); +}); + +add_task(async function test_login_with_different_subdomain_shouldnt_autofill_different_domain_formActionOrigin() { + await prepareLogins([ + new nsLoginInfo(otherOrigin, "https://example.net", null, + "name2", "pass2", "uname", "pword"), + ]); + await formReadyInWindow(origin + MISSING_ACTION_PATH); + + await checkWindowLoginForm("", ""); +}); + +add_task(async function test_login_with_same_origin_shouldnt_autofill_cross_origin_iframe() { + await SimpleTest.promiseFocus(window); + + async function checkIframeLoginForm(expectedUsername, expectedPassword) { + return SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [expectedUsername, expectedPassword], function(un, pw) { + var u = this.content.document.getElementById("form-basic-username"); + var p = this.content.document.getElementById("form-basic-password"); + Assert.equal(u.value, un, "Check username value"); + Assert.equal(p.value, pw, "Check password value"); + }); + } + + // We need an origin that is supported by the test framework to be able to load the + // cross-origin form into the iframe. + let crossOrigin = "https://test1.example.com"; + info(`Top level frame origin: ${origin}. Iframe and login origin: ${crossOrigin}.`); + await prepareLogins([ + new nsLoginInfo(crossOrigin, crossOrigin, null, + "name2", "pass2", "uname", "pword"), + ]); + await formReadyInFrame(crossOrigin + MISSING_ACTION_PATH); + + await checkIframeLoginForm("", ""); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_from_bfcache.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_from_bfcache.html new file mode 100644 index 0000000000..d0fcb16e18 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_from_bfcache.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofilling documents restored from the back/forward cache (bfcache)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="test_crossOriginBfcacheRestore();"> +<p id="display"></p> + +<div id="content"> + <a id="next" href="https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/file_history_back.html" target="loginWin">Next</a> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/* + * The test opens a new window and updates login information. Then + * a new page is loaded and it goes immediately back. The initial page + * should be coming out from the bfcache and the form control values should be + * the ones filled during the initial load. + */ +async function test_crossOriginBfcacheRestore() { + // If Fission is disabled, the pref is no-op. + await SpecialPowers.pushPrefEnv({set: [["fission.bfcacheInParent", true]]}); + + var bc = new BroadcastChannel("form_basic_bfcache"); + window.open("form_basic_bfcache.html", "", "noopener"); + var pageshowCount = 0; + bc.onmessage = function(event) { + if (event.data.type == "pageshow") { + ++pageshowCount; + if (pageshowCount == 1) { + is(event.data.persisted, false, "Initial load"); + bc.postMessage("nextpage"); + } else if (pageshowCount == 2) { + is(event.data.persisted, true, "Should have persisted the page."); + bc.postMessage("close"); + } + } else if (event.data.type == "is") { + is(event.data.value1, event.data.value2, event.data.message); + } else if (event.data.type == "ok") { + is(event.data.value, event.data.message); + } else if (event.data == "closed") { + bc.close(); + SimpleTest.finish(); + } + } + +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_hasBeenTypePassword.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_hasBeenTypePassword.html new file mode 100644 index 0000000000..2139d30e61 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_hasBeenTypePassword.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test no autofill into a password field that is no longer type=password</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +Login Manager test: Test no autofill into a password field that is no longer type=password + +<script> +let DEFAULT_ORIGIN = window.location.origin; + +/** Test for Login Manager: Test no autofill into a password field that is no longer type=password **/ + +add_setup(async () => { + await setStoredLoginsAsync( + [DEFAULT_ORIGIN, "https://autofill", null, "user1", "pass1"] + ); +}); + + +// As a control, test that autofill is working on this page. +add_task(async function test_autofill_control() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <form id="form1" action="https://autofill"> + <p>This is form 1.</p> + <input id="username-1" type="text" name="uname"> + <input id="password-1" type="password" name="pword"> + + <button type="submit">Submit</button> + </form>`, win); + await checkLoginFormInFrame(win, "username-1", "user1", "password-1", "pass1"); +}); + +add_task(async function test_no_autofill() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + // Synchronously change the password field type to text before the fill happens. + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <form id="form1" action="https://autofill"> + <p>This is form 1.</p> + <input id="username-1" type="text" name="uname"> + <input id="password-1" type="password" name="pword"> + + <button type="submit">Submit</button> + </form>`, win, 1, () => { + this.content.document.getElementById("password-1").type = "text"; + }); + await checkLoginFormInFrame(win, "username-1", "", "password-1", ""); +}); +</script> + +<p id="display"></p> + +<div id="content"></div> + +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight.html new file mode 100644 index 0000000000..3ca214841c --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test form field autofill highlight</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +<pre id="test"> +<script> +add_setup(async () => { + await setStoredLoginsAsync( + [window.location.origin, "https://autofill", null, "user1", "pass1", "", ""] + ); +}); + +add_task(async function test_field_highlight_on_autofill() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(window.location.origin, ` + <form id="form1" action="https://autofill" onsubmit="return false;"> + <input type="text" id="uname"> + <input type="password" id="pword"> + <button type="submit">Submit</button> + </form>`, win); + + await SpecialPowers.spawn(win, [], async function() { + let EventUtils = ContentTaskUtils.getEventUtils(this.content); + let doc = this.content.document; + let username = doc.getElementById("uname"); + let password = doc.getElementById("pword"); + ok(username.matches(":autofill"), + "Highlight was successfully applied to the username field on page load autofill"); + ok(password.matches(":autofill"), + "Highlight was successfully applied to the password field on page load autofill"); + + // Test that initiating a change on the input value will remove the highlight. We check by pressing + // the tab key after backspace(by shifting focus to the next element) because the tab key is known to + // cause a bug where the highlight is applied once again. + username.focus(); + await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content); + await EventUtils.synthesizeKey("KEY_Tab", {}, this.content); + + ok(!username.matches(":autofill"), "Highlight was successfully removed on change in value of username input element"); + + await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content); + await EventUtils.synthesizeKey("KEY_Tab", {}, this.content); + ok(!password.matches(":autofill"), "Highlight was successfully removed on change in value of password input element"); + }); +}); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_empty_username.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_empty_username.html new file mode 100644 index 0000000000..36fffb480b --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_empty_username.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test that filling an empty username into a form does not highlight the username element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +</script> +<body> +<p id="display"></p> +<div id="content"> +<pre id="test"> +<script> +add_setup(async () => { + await setStoredLoginsAsync( + [window.location.origin, "https://autofill", null, "", "pass1", "", ""] + ); +}); + +add_task(async function test_field_highlight_on_autofill() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(window.location.origin, ` + <form id="form1" action="https://autofill" onsubmit="return false;"> + <input type="text" id="uname"> + <input type="password" id="pword"> + <button type="submit">Submit</button> + </form>`, win); + + await SpecialPowers.spawn(win, [], async function() { + let EventUtils = ContentTaskUtils.getEventUtils(this.content); + let doc = this.content.document; + let username = doc.getElementById("uname"); + let password = doc.getElementById("pword"); + ok(!username.matches(":autofill"), + "Highlight was not applied to the username field on page load autofill"); + ok(password.matches(":autofill"), + "Highlight was successfully applied to the password field on page load autofill"); + + // Test that initiating a change on the input value will remove the highlight. We check by pressing + // the tab key after backspace(by shifting focus to the next element) because the tab key is known to + // cause a bug where the highlight is applied once again. + username.focus(); + await EventUtils.synthesizeKey("U", {}, this.content); + await EventUtils.synthesizeKey("KEY_Tab", {}, this.content); + + ok(!username.matches(":autofill"), "Highlight is still not present on username element"); + + await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content); + await EventUtils.synthesizeKey("KEY_Tab", {}, this.content); + ok(!password.matches(":autofill"), "Highlight was successfully removed on change in value of password input element"); + }); +}); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_username_only_form.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_username_only_form.html new file mode 100644 index 0000000000..67892a9ea4 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_highlight_username_only_form.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test that filling a username into a username-only form does highlight the username element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +<pre id="test"> +<script> +add_setup(async () => { + await setStoredLoginsAsync( + [window.location.origin, "https://autofill", null, "user1", "pass1", "", ""] + ); +}); + +add_task(async function test_field_highlight_on_autofill() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(window.location.origin, ` + <form id="form1" action="https://autofill" onsubmit="return false;"> + <input type="text" id="uname" autocomplete="username"> + <button type="submit">Submit</button> + </form>`, win); + + await SpecialPowers.spawn(win, [], async function() { + let EventUtils = ContentTaskUtils.getEventUtils(this.content); + let doc = this.content.document; + let username = doc.getElementById("uname"); + ok(username.matches(":autofill"), + "Highlight was successfully applied to the username field on page load autofill"); + + // Test that initiating a change on the input value will remove the highlight. We check by pressing + // the tab key after backspace(by shifting focus to the next element) because the tab key is known to + // cause a bug where the highlight is applied once again. + username.focus(); + await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content); + await EventUtils.synthesizeKey("KEY_Tab", {}, this.content); + + ok(!username.matches(":autofill"), "Highlight was successfully removed on change in value of username input element"); + }); +}); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_downgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_downgrade.html new file mode 100644 index 0000000000..091f9c8ad6 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_downgrade.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test we don't autofill on an HTTP page using HTTPS logins</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html"; +const SAME_ORIGIN_ACTION_PATH = TESTS_DIR + "mochitest/form_same_origin_action.html"; + +const chromeScript = runChecksAfterCommonInit(false); + +let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1", + SpecialPowers.Ci.nsILoginInfo, + "init"); +</script> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +let win = window.open("about:blank"); +SimpleTest.registerCleanupFunction(() => win.close()); + +async function prepareAndProcessForm(url, login) { + let processedPromise = promiseFormsProcessed(); + win.location = url; + info("prepareAndProcessForm, assigned window location: " + url); + await processedPromise; +} + +async function checkFormsWithLogin(formUrls, login, expectedUsername, expectedPassword) { + await LoginManager.removeAllUserFacingLogins(); + await LoginManager.addLoginAsync(login); + + for (let url of formUrls) { + info("start test_checkNoAutofillOnDowngrade w. url: " + url); + + await prepareAndProcessForm(url); + info("form was processed"); + + await SpecialPowers.spawn(win, [url, expectedUsername, expectedPassword], + function(urlContent, expectedUsernameContent, expectedPasswordContent) { + let doc = this.content.document; + let uname = doc.getElementById("form-basic-username"); + let pword = doc.getElementById("form-basic-password"); + Assert.equal(uname.value, expectedUsernameContent, `username ${expectedUsernameContent ? "filled" : "not filled"} on ${urlContent}`); + Assert.equal(pword.value, expectedPasswordContent, `password ${expectedPasswordContent ? "filled" : "not filled"} on ${urlContent}`); + }); + } +} + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.schemeUpgrades", true], + ["dom.security.https_first", false], + ]}); +}); + +add_task(async function test_sanityCheckHTTPS() { + let login = new nsLoginInfo("https://example.com", "https://example.com", null, + "name1", "pass1", "uname", "pword"); + + await checkFormsWithLogin([ + `https://example.com${MISSING_ACTION_PATH}`, + `https://example.com${SAME_ORIGIN_ACTION_PATH}`, + ], login, "name1", "pass1"); +}); + +add_task(async function test_checkNoAutofillOnDowngrade() { + let login = new nsLoginInfo("https://example.com", "https://example.com", null, + "name1", "pass1", "uname", "pword"); + await checkFormsWithLogin([ + `http://example.com${MISSING_ACTION_PATH}`, + `http://example.com${SAME_ORIGIN_ACTION_PATH}`, + ], login, "", ""); +}); + +add_task(async function test_checkNoAutofillOnDowngradeSubdomain() { + let login = new nsLoginInfo("https://sub.example.com", "https://example.com", null, + "name1", "pass1", "uname", "pword"); + todo(false, "await promiseFormsProcessed timesout when test is run with scheme=https"); + await checkFormsWithLogin([ + `http://example.com${MISSING_ACTION_PATH}`, + `http://example.com${SAME_ORIGIN_ACTION_PATH}`, + ], login, "", ""); +}); + + +add_task(async function test_checkNoAutofillOnDowngradeDifferentPort() { + let login = new nsLoginInfo("https://example.com:8080", "https://example.com", null, + "name1", "pass1", "uname", "pword"); + await checkFormsWithLogin([ + `http://example.com${MISSING_ACTION_PATH}`, + `http://example.com${SAME_ORIGIN_ACTION_PATH}`, + ], login, "", ""); +}); + +add_task(async function test_checkNoAutofillOnDowngradeSubdomainDifferentPort() { + let login = new nsLoginInfo("https://sub.example.com:8080", "https://example.com", null, + "name1", "pass1", "uname", "pword"); + await checkFormsWithLogin([ + `https://example.com${MISSING_ACTION_PATH}`, + `https://example.com${SAME_ORIGIN_ACTION_PATH}`, + ], login, "", ""); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html new file mode 100644 index 0000000000..a8691ebe54 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_https_upgrade.html @@ -0,0 +1,148 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofill on an HTTPS page using upgraded HTTP logins</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +const MISSING_ACTION_PATH = TESTS_DIR + "mochitest/form_basic.html"; +const CROSS_ORIGIN_SECURE_PATH = TESTS_DIR + "mochitest/form_cross_origin_secure_action.html"; + +const chromeScript = runChecksAfterCommonInit(false); + +let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1", + SpecialPowers.Ci.nsILoginInfo, + "init"); +</script> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +let win = window.open("about:blank"); +SimpleTest.registerCleanupFunction(() => win.close()); + +async function prepareLoginsAndProcessForm(url, logins = []) { + await LoginManager.removeAllUserFacingLogins(); + + let dates = Date.now(); + for (let login of logins) { + SpecialPowers.do_QueryInterface(login, SpecialPowers.Ci.nsILoginMetaInfo); + // Force all dates to be the same so they don't affect things like deduping. + login.timeCreated = login.timePasswordChanged = login.timeLastUsed = dates; + await LoginManager.addLoginAsync(login); + } + + let processedPromise = promiseFormsProcessed(); + win.location = url; + await processedPromise; +} + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.schemeUpgrades", true], + ["signon.includeOtherSubdomainsInLookup", true], + ]}); +}); + +add_task(async function test_simpleNoDupesNoAction() { + await prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [ + new nsLoginInfo("http://example.com", "http://example.com", null, + "name2", "pass2", "uname", "pword"), + ]); + + await checkLoginFormInFrame(win, + "form-basic-username", "name2", + "form-basic-password", "pass2"); +}); + +add_task(async function test_simpleNoDupesUpgradeOriginAndAction() { + await prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [ + new nsLoginInfo("http://example.com", "http://example.org", null, + "name2", "pass2", "uname", "pword"), + ]); + + await checkLoginFormInFrame(win, "form-basic-username", "name2", + "form-basic-password", "pass2"); +}); + +add_task(async function test_simpleNoDupesUpgradeOriginOnly() { + await prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [ + new nsLoginInfo("http://example.com", "https://example.org", null, + "name2", "pass2", "uname", "pword"), + ]); + + await checkLoginFormInFrame(win, "form-basic-username", "name2", + "form-basic-password", "pass2"); +}); + +add_task(async function test_simpleNoDupesUpgradeActionOnly() { + await prepareLoginsAndProcessForm("https://example.com" + CROSS_ORIGIN_SECURE_PATH, [ + new nsLoginInfo("https://example.com", "http://example.org", null, + "name2", "pass2", "uname", "pword"), + ]); + + await checkLoginFormInFrame(win, "form-basic-username", "name2", + "form-basic-password", "pass2"); +}); + +add_task(async function test_dedupe() { + await prepareLoginsAndProcessForm("https://example.com" + MISSING_ACTION_PATH, [ + new nsLoginInfo("https://example.com", "https://example.com", null, + "name1", "passHTTPStoHTTPS", "uname", "pword"), + new nsLoginInfo("http://example.com", "http://example.com", null, + "name1", "passHTTPtoHTTP", "uname", "pword"), + new nsLoginInfo("http://example.com", "https://example.com", null, + "name1", "passHTTPtoHTTPS", "uname", "pword"), + new nsLoginInfo("https://example.com", "http://example.com", null, + "name1", "passHTTPStoHTTP", "uname", "pword"), + ]); + + await checkLoginFormInFrame(win, "form-basic-username", "name1", + "form-basic-password", "passHTTPStoHTTPS"); +}); + +add_task(async function test_dedupe_subdomain() { + // subdomain match (should be autofilled) + let loginToFill = new nsLoginInfo("http://test1.example.com", "http://test1.example.com", null, + "name1", "pass1"); + const loginToFillGUID = "subdomain-match" + // Assign a GUID to this login so we can ensure this is the login that gets + // filled later. + loginToFill.QueryInterface(SpecialPowers.Ci.nsILoginMetaInfo).guid = loginToFillGUID; + + await prepareLoginsAndProcessForm("https://test1.example.com" + MISSING_ACTION_PATH, [ + // All logins have the same username and password: + // https: (scheme match) + new nsLoginInfo("https://example.com", "https://example.com", null, + "name1", "pass1"), + loginToFill, + // formActionOrigin match + new nsLoginInfo("http://example.com", "https://test1.example.com", null, + "name1", "pass1"), + ]); + + await checkLoginFormInFrame(win, "form-basic-username", "name1", + "form-basic-password", "pass1"); + + let filledGUID = await SpecialPowers.spawn(win, [], function getFilledGUID() { + let LMC = this.content.windowGlobalChild.getActor("LoginManager"); + let doc = this.content.document; + let form = doc.getElementById("form-basic"); + let { login: filledLogin } = LMC.stateForDocument(doc).fillsByRootElement.get(form); + return filledLogin && filledLogin.guid; + }); + is(filledGUID, loginToFillGUID, "Check the correct login was filled"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html new file mode 100644 index 0000000000..47f53bb65b --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_password-only.html @@ -0,0 +1,133 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test password-only forms should prefer a password-only login when present</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: Bug 444968 +<script> +runChecksAfterCommonInit(null); + +SimpleTest.waitForExplicitFinish(); + +const chromeScript = runInParent(async function chromeSetup() { + const login1A = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + const login1B = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + const login2A = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + const login2B = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + const login2C = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + + login1A.init("http://mochi.test:8888", "http://bug444968-1", null, "testuser1A", "testpass1A", "", ""); + login1B.init("http://mochi.test:8888", "http://bug444968-1", null, "", "testpass1B", "", ""); + + login2A.init("http://mochi.test:8888", "http://bug444968-2", null, "testuser2A", "testpass2A", "", ""); + login2B.init("http://mochi.test:8888", "http://bug444968-2", null, "", "testpass2B", "", ""); + login2C.init("http://mochi.test:8888", "http://bug444968-2", null, "testuser2C", "testpass2C", "", ""); + + await Services.logins.addLogins([ + login1A, + login1B, + login2A, + login2B, + login2C, + ]); + + addMessageListener("removeLogins", function removeLogins() { + Services.logins.removeLogin(login1A); + Services.logins.removeLogin(login1B); + Services.logins.removeLogin(login2A); + Services.logins.removeLogin(login2B); + Services.logins.removeLogin(login2C); + }); +}); + +SimpleTest.registerCleanupFunction(() => chromeScript.sendAsyncMessage("removeLogins")); +</script> + +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +const DEFAULT_ORIGIN = window.location.origin; + +/* Test for Login Manager: 444968 (password-only forms should prefer a + * password-only login when present ) + */ +async function startTest() { + const win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <!-- first 3 forms have matching user+pass and pass-only logins --> + + <!-- user+pass form. --> + <form id="form1" action="http://bug444968-1"> + <input type="text" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <!-- password-only form. --> + <form id="form2" action="http://bug444968-1"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <!-- user+pass form, username prefilled --> + <form id="form3" action="http://bug444968-1"> + <input type="text" name="uname" value="testuser1A"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + + <!-- next 4 forms have matching user+pass (2x) and pass-only (1x) logins --> + + <!-- user+pass form. --> + <form id="form4" action="http://bug444968-2"> + <input type="text" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <!-- password-only form. --> + <form id="form5" action="http://bug444968-2"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <!-- user+pass form, username prefilled --> + <form id="form6" action="http://bug444968-2"> + <input type="text" name="uname" value="testuser2A"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <!-- user+pass form, username prefilled --> + <form id="form7" action="http://bug444968-2"> + <input type="text" name="uname" value="testuser2C"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form>`, win, 7); + + await checkLoginFormInFrameWithElementValues(win, 1, "testuser1A", "testpass1A"); + await checkLoginFormInFrameWithElementValues(win, 2, "testpass1B"); + await checkLoginFormInFrameWithElementValues(win, 3, "testuser1A", "testpass1A"); + + checkUnmodifiedFormInFrame(win, 4); // 2 logins match + await checkLoginFormInFrameWithElementValues(win, 5, "testpass2B"); + await checkLoginFormInFrameWithElementValues(win, 6, "testuser2A", "testpass2A"); + await checkLoginFormInFrameWithElementValues(win, 7, "testuser2C", "testpass2C"); + + SimpleTest.finish(); +} + +registerRunTests(0, startTest); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html new file mode 100644 index 0000000000..ef4dfdb417 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_sandboxed.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test form field autofill in sandboxed documents (null principal)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <iframe id="sandboxed" + sandbox=""></iframe> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const { TestUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +/** Test for Login Manager: form field autofill in sandboxed documents (null principal) **/ + +const sandboxed = document.getElementById("sandboxed"); +let uname; +let pword; + +add_setup(async () => { + await setStoredLoginsAsync(["https://example.com", "", null, "tempuser1", "temppass1", "uname", "pword"]); +}); + +add_task(async function test_no_autofill_in_form() { + sandboxed.src = "form_basic.html"; + const frameWindow = SpecialPowers.wrap(sandboxed).contentWindow; + const DOMFormHasPasswordPromise = new Promise(resolve => { + SpecialPowers.addChromeEventListener("DOMFormHasPassword", function onDFHP() { + SpecialPowers.removeChromeEventListener("DOMFormHasPassword", onDFHP); + resolve(); + }); + }); + // Can't use SimpleTest.promiseFocus as it doesn't work with the sandbox. + await SimpleTest.promiseWaitForCondition(() => { + return frameWindow.document.readyState == "complete" && + frameWindow.location.href.endsWith("form_basic.html"); + }, "Check frame is loaded"); + info("frame loaded"); + await DOMFormHasPasswordPromise; + const frameDoc = SpecialPowers.wrap(sandboxed).contentDocument; + + uname = frameDoc.getElementById("form-basic-username"); + pword = frameDoc.getElementById("form-basic-password"); + + // Autofill shouldn't happen in the sandboxed frame but would have happened by + // now since DOMFormHasPassword was observed above. + await ensureLoginFormStaysFilledWith(uname, "", pword, ""); + + info("blurring the username field after typing the username"); + uname.focus(); + uname.setUserInput("tempuser1"); + synthesizeKey("VK_TAB", {}, frameWindow); + + await TestUtils.waitForCondition(() => { + return uname.value === "tempuser1" & pword.value === ""; + }, "Username and password field should be filled"); +}); + +add_task(async function test_no_autofill_outside_form() { + sandboxed.src = "formless_basic.html"; + const frameWindow = SpecialPowers.wrap(sandboxed).contentWindow; + const DOMInputPasswordAddedPromise = new Promise(resolve => { + SpecialPowers.addChromeEventListener("DOMInputPasswordAdded", function onDIPA() { + SpecialPowers.removeChromeEventListener("DOMInputPasswordAdded", onDIPA); + resolve(); + }); + }); + // Can't use SimpleTest.promiseFocus as it doesn't work with the sandbox. + await SimpleTest.promiseWaitForCondition(() => { + return frameWindow.document.readyState == "complete" && + frameWindow.location.href.endsWith("formless_basic.html"); + }, "Check frame is loaded"); + info("frame loaded"); + await DOMInputPasswordAddedPromise; + const frameDoc = SpecialPowers.wrap(sandboxed).contentDocument; + + uname = frameDoc.getElementById("form-basic-username"); + pword = frameDoc.getElementById("form-basic-password"); + + // Autofill shouldn't happen in the sandboxed frame but would have happened by + // now since DOMInputPasswordAdded was observed above. + await ensureLoginFormStaysFilledWith(uname, "", pword, ""); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_tab_between_fields.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_tab_between_fields.html new file mode 100644 index 0000000000..c88a6713f4 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_tab_between_fields.html @@ -0,0 +1,151 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autocomplete behavior when tabbing between form fields</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +let nsLoginInfo = SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1", + SpecialPowers.Ci.nsILoginInfo, + "init"); +let readyPromise = registerRunTests(); +</script> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +let DEFAULT_ORIGIN = window.location.origin; +let win; +let html = ` + <form id="form1" action="https://autofill" onsubmit="return false;"> + <input type="text" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form>`; + +async function prepareLogins(logins = []) { + await LoginManager.removeAllUserFacingLogins(); + + for (let login of logins) { + await LoginManager.addLoginAsync(login); + } + let count = (await LoginManager.getAllLogins()).length; + is(count, logins.length, "All logins were added"); +} + +const availableLogins = { + "exampleUser1": new nsLoginInfo(DEFAULT_ORIGIN, "https://autofill", null, + "user1", "pass1", "uname", "pword"), +} + +async function recreateTreeInWindow(formNum) { + await SpecialPowers.spawn(win, [formNum], (formNumF) => { + let form = this.content.document.querySelector(`#form${formNumF}`); + // eslint-disable-next-line no-self-assign + form.outerHTML = form.outerHTML; + }); +} + +const tests = [ + { + name: "autofill_disabled_exact_username", + autofillEnabled: false, + logins: ["exampleUser1"], + expectedAutofillUsername: "", + expectedAutofillPassword: "", + typeUsername: "user1", + expectedTabbedUsername: "user1", + expectedTabbedPassword: "", + }, + { + name: "autofill_enabled_exact_username", + autofillEnabled: true, + logins: ["exampleUser1"], + expectedAutofillUsername: "user1", + expectedAutofillPassword: "pass1", + typeUsername: "user1", + expectedTabbedUsername: "user1", + expectedTabbedPassword: "pass1", + }, +]; + +add_setup(async () => { + ok(readyPromise, "check promise is available"); + await readyPromise; + win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, html, win); +}); + +async function testResultOfTabInteractions(testData) { + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.autofillForms", testData.autofillEnabled], + ]}); + + await SimpleTest.promiseFocus(win); + let logins = testData.logins.map(name => availableLogins[name]); + await prepareLogins(logins); + + info("recreating form"); + let processed = promiseFormsProcessed(); + await recreateTreeInWindow(1); + info("waiting for form processed"); + await processed; + // check autofill results + await checkLoginFormInFrameWithElementValues(win, 1, testData.expectedAutofillUsername, testData.expectedAutofillPassword); + + await SpecialPowers.spawn(win, [testData.typeUsername], async (typeUsername) => { + let doc = this.content.document; + let pword = doc.querySelector("[name='pword']"); + let uname = doc.querySelector("[name='uname']"); + + pword.setUserInput(""); + uname.setUserInput(""); + + info("Placing focus in the username field"); + uname.focus(); + + if (typeUsername) { + info("Filling username field"); + EventUtils.sendString(typeUsername, this.content); + } + + EventUtils.synthesizeKey("KEY_Tab", {}, this.content); // blur un, focus pw + await new Promise(resolve => SpecialPowers.executeSoon(resolve)); + + ok(pword.matches("input:focus"), "pword field is focused"); + }); + + await checkLoginFormInFrameWithElementValues(win, 1, testData.expectedTabbedUsername, testData.expectedTabbedPassword); + + await recreateTreeInWindow(1); + await promiseFormsProcessed(); + + await SpecialPowers.spawn(win, [], () => { + EventUtils.synthesizeKey("KEY_Escape", {}, this.content); + }); +} + +for (let testData of tests) { + let tmp = { + async [testData.name]() { + await testResultOfTabInteractions(testData); + }, + }; + add_task(tmp[testData.name]); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only.html new file mode 100644 index 0000000000..860c317409 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofill on username-form</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Test autofill on username-form + +<script> +add_setup(async () => { + await setStoredLoginsAsync( + [window.location.origin, "https://autofill", null, "user1", "pass1"] + ); +}); + +add_task(async function test_autofill_username_only_form() { + await loadRecipes({ + siteRecipes: [{ + hosts: ["mochi.test:8888"], + notUsernameSelector: "input[name='shouldnotfill']", + }], + }); + + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + + // 5 out of the 7 forms should be autofilled + await loadFormIntoWindow(window.location.origin, ` + <!-- no password field, 1 username field --> + <form id='form1' action='https://autofill'> 1 + <input type='text' name='uname' autocomplete='username' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- no password field, 1 username field, with a value set --> + <form id='form2' action='https://autofill'> 2 + <input type='text' name='uname' autocomplete='username' value='someuser'> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- no password field, 2 username fields, should be ignored --> + <form id='form3' action='https://autofill'> 3 + <input type='text' name='uname1' autocomplete='username' value=''> + <input type='text' name='uname2' autocomplete='username' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- no password field, 1 username field, too small for the username login --> + <form id='form4' action='https://autofill'> 4 + <input type='text' name='uname' value='' maxlength="4" autocomplete='username'> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- no password field, 1 username field, too small for the username login --> + <form id='form5' action='https://autofill'> 5 + <input type='text' name='uname' value='' maxlength="0" autocomplete='username'> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- no password field, 1 text input field (not a username-only form), should be ignored --> + <form id='form6' action='https://autofill'> 6 + <input type='text' name='uname' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- no password field, 1 username field that matches notUsernameSelector recipe --> + <form id='form7' action='https://autofill'> 7 + <input type='text' name='shouldnotfill' autocomplete='username' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form>`, win, 5); + + await checkLoginFormInFrameWithElementValues(win, 1, "user1"); + await checkLoginFormInFrameWithElementValues(win, 2, "someuser"); + await checkUnmodifiedFormInFrame(win, 3); + await checkUnmodifiedFormInFrame(win, 4); + await checkUnmodifiedFormInFrame(win, 5); + await checkUnmodifiedFormInFrame(win, 6); + await checkUnmodifiedFormInFrame(win, 7); + + await resetRecipes(); +}); +</script> + +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only_threshold.html b/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only_threshold.html new file mode 100644 index 0000000000..d9a2e08095 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_username-only_threshold.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofill on username-form when the number of form exceeds the lookup threshold</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Test not autofill on username-form when the number of form exceeds the lookup threshold + +<script> +add_setup(async () => { + await SpecialPowers.pushPrefEnv({"set": [["signon.usernameOnlyForm.lookupThreshold", 5]]}); + + await setStoredLoginsAsync( + [window.location.origin, "https://autofill", null, "user1", "pass1"] + ); +}); + +add_task(async function test_autofill_username_only_form() { + const win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + + await loadFormIntoWindow(window.location.origin, ` + <!-- no password field, 1 username field --> + <form id='form1' action='https://autofill'> 1 + <input type='text' name='uname' autocomplete='username' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <form id='form2' action='https://autofill'> 2 + <input type='text' name='uname' autocomplete='username' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <form id='form3' action='https://autofill'> 3 + <input type='text' name='uname' autocomplete='username' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <form id='form4' action='https://autofill'> 4 + <input type='text' name='uname' autocomplete='username' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <form id='form5' action='https://autofill'> 5 + <input type='text' name='uname' autocomplete='username' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <form id='form6' action='https://autofill'> 6 + <input type='text' name='uname' autocomplete='username' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form>`, win, 5); + + await checkLoginFormInFrameWithElementValues(win, 1, "user1"); + await checkLoginFormInFrameWithElementValues(win, 2, "user1"); + await checkLoginFormInFrameWithElementValues(win, 3, "user1"); + await checkLoginFormInFrameWithElementValues(win, 4, "user1"); + await checkLoginFormInFrameWithElementValues(win, 5, "user1"); + await checkUnmodifiedFormInFrame(win, 6); +}); +</script> + +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html new file mode 100644 index 0000000000..803197c2a2 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autofocus_js.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test login autocomplete is activated when focused by js on load</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <iframe></iframe> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +const iframe = document.getElementsByTagName("iframe")[0]; +let iframeDoc, hostname; + +add_setup(async () => { + const origin = window.location.origin; + await setStoredLoginsAsync( + [origin, origin, null, "name", "pass"], + [origin, origin, null, "name1", "pass1"] + ); + + const processedPromise = promiseFormsProcessed(); + iframe.src = "/tests/toolkit/components/passwordmgr/test/mochitest/form_autofocus_js.html"; + await new Promise(resolve => { + iframe.addEventListener("load", function() { + resolve(); + }, {once: true}); + }); + + await processedPromise; + + hostname = await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + return this.content.document.documentURIObject.host; + }); + + SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popupshown to occur"); +}); + +add_task(async function test_initial_focus() { + let results = await notifyMenuChanged(3, "name"); + checkAutoCompleteResults(results, ["name", "name1"], hostname, "Two login results"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await promiseFormsProcessedInSameProcess(); + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + Assert.equal(this.content.document.getElementById("form-basic-password").value, "pass", "Check first password filled"); + }); + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is now closed"); +}); + +// This depends on the filling from the previous test. +add_task(async function test_not_reopened_if_filled() { + listenForUnexpectedPopupShown(); + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + this.content.document.getElementById("form-basic-username").focus(); + }); + info("Waiting to see if a popupshown occurs"); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // cleanup + gPopupShownExpected = true; + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + this.content.document.getElementById("form-basic-submit").focus(); + }); +}); + +add_task(async function test_reopened_after_edit_not_matching_saved() { + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + this.content.document.getElementById("form-basic-username").value = "nam"; + }); + await popupBy(async () => { + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + this.content.document.getElementById("form-basic-username").focus(); + }); + }); + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + this.content.document.getElementById("form-basic-submit").focus(); + }); +}); + +add_task(async function test_not_reopened_after_selecting() { + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + this.content.document.getElementById("form-basic-username").value = ""; + this.content.document.getElementById("form-basic-password").value = ""; + }); + listenForUnexpectedPopupShown(); + + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + let formFillController = SpecialPowers.getFormFillController(); + let usernameField = this.content.document.getElementById("form-basic-username"); + formFillController.markAsLoginManagerField(usernameField); + }); + + info("Waiting to see if a popupshown occurs"); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Cleanup + gPopupShownExpected = true; +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html new file mode 100644 index 0000000000..d13fd1369f --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test basic autofill</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: simple form fill + +<script> +gTestDependsOnDeprecatedLogin = true; +runChecksAfterCommonInit(startTest); + +let DEFAULT_ORIGIN = window.location.origin; + +/** Test for Login Manager: form fill, multiple forms. **/ + +async function startTest() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <form id="form1" action="formtest.js"> + <p>This is form 1.</p> + <input id="username-1" type="text" name="uname"> + <input id="password-1" type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form>`, win); + await checkLoginFormInFrame(win, "username-1", "testuser", "password-1", "testpass"); + + SimpleTest.finish(); +} +</script> + +<p id="display"></p> + +<div id="content" style="display: none"> + + +</div> + +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html new file mode 100644 index 0000000000..64450300d6 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_0pw.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test forms with no password fields</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: forms with no password fields +<p id="display"></p> + +<div id="content" style="display: none"> + + <!-- Form with no user field or password field --> + <form id="form1" action="formtest.js"> + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Form with no user field or password field, but one other field --> + <form id="form2" action="formtest.js"> + <input type="checkbox"> + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Form with no user field or password field, but one other field --> + <form id="form3" action="formtest.js"> + <input type="checkbox" name="uname" value=""> + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Form with a text field, but no password field --> + <form id="form4" action="formtest.js"> + <input type="text" name="yyyyy"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Form with a user field, but no password field --> + <form id="form5" action="formtest.js"> + <input type="text" name="uname"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function startTest() { + is(getFormElementByName(3, "uname").value, "", "Checking for unfilled checkbox (form 3)"); + is(getFormElementByName(4, "yyyyy").value, "", "Checking for unfilled text field (form 4)"); + is(getFormElementByName(5, "uname").value, "", "Checking for unfilled text field (form 5)"); + + SimpleTest.finish(); +} + +runChecksAfterCommonInit(startTest); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html new file mode 100644 index 0000000000..bb91d9cf3e --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw.html @@ -0,0 +1,171 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofill for forms with 1 password field</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: forms with 1 password field +<script> +gTestDependsOnDeprecatedLogin = true; +runChecksAfterCommonInit(() => startTest()); + +let DEFAULT_ORIGIN = window.location.origin; +</script> +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: simple form fill **/ + +async function startTest() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <!-- no username fields --> + + <form id='form1' action='formtest.js'> 1 + <!-- Blank, so fill in the password --> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form2' action='formtest.js'> 2 + <!-- Already contains the password, so nothing to do. --> + <input type='password' name='pname' value='testpass'> + <button type='submit'>Submit</button> + </form> + + <form id='form3' action='formtest.js'> 3 + <!-- Contains unknown password, so don't change it --> + <input type='password' name='pname' value='xxxxxxxx'> + <button type='submit'>Submit</button> + </form> + + + <!-- username fields --> + + <form id='form4' action='formtest.js'> 4 + <!-- Blanks, so fill in login --> + <input type='text' name='uname' value=''> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form5' action='formtest.js'> 5 + <!-- Username already set, so fill in password --> + <input type='text' name='uname' value='testuser'> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form6' action='formtest.js'> 6 + <!-- Unknown username, so don't fill in password --> + <input type='text' name='uname' value='xxxxxxxx'> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form7' action='formtest.js'> 7 + <!-- Password already set, could fill in username but that's weird so we don't --> + <input type='text' name='uname' value=''> + <input type='password' name='pname' value='testpass'> + <button type='submit'>Submit</button> + </form> + + <form id='form8' action='formtest.js'> 8 + <!-- Unknown password, so don't fill in a username --> + <input type='text' name='uname' value=''> + <input type='password' name='pname' value='xxxxxxxx'> + <button type='submit'>Submit</button> + </form> + + + + <!-- extra text fields --> + + <form id='form9' action='formtest.js'> 9 + <!-- text field _after_ password should never be treated as a username field --> + <input type='password' name='pname' value=''> + <input type='text' name='uname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form10' action='formtest.js'> 10 + <!-- only the first text field before the password should be for username --> + <input type='text' name='other' value=''> + <input type='text' name='uname' value=''> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form11' action='formtest.js'> 11 + <!-- variation just to make sure extra text field is still ignored --> + <input type='text' name='uname' value=''> + <input type='password' name='pname' value=''> + <input type='text' name='other' value=''> + <button type='submit'>Submit</button> + </form> + + + + <!-- same as last bunch, but with xxxx in the extra field. --> + + <form id='form12' action='formtest.js'> 12 + <!-- text field _after_ password should never be treated as a username field --> + <input type='password' name='pname' value=''> + <input type='text' name='uname' value='xxxxxxxx'> + <button type='submit'>Submit</button> + </form> + + <form id='form13' action='formtest.js'> 13 + <!-- only the first text field before the password should be for username --> + <input type='text' name='other' value='xxxxxxxx'> + <input type='text' name='uname' value=''> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form14' action='formtest.js'> 14 + <!-- variation just to make sure extra text field is still ignored --> + <input type='text' name='uname' value=''> + <input type='password' name='pname' value=''> + <input type='text' name='other' value='xxxxxxxx'> + <button type='submit'>Submit</button> + </form>`, win, 14); + + var f = 1; + + // 1-3 + await checkLoginFormInFrameWithElementValues(win, f++, "testpass"); + await checkLoginFormInFrameWithElementValues(win, f++, "testpass"); + await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx"); + + // 4-8 + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass"); + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass"); + await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "", "testpass"); + await checkLoginFormInFrameWithElementValues(win, f++, "", "xxxxxxxx"); + + // 9-14 + await checkLoginFormInFrameWithElementValues(win, f++, "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "", "testuser", "testpass"); + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "xxxxxxxx"); + await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", "testuser", "testpass"); + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", "xxxxxxxx"); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html new file mode 100644 index 0000000000..d7aaadc895 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_1pw_2.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test forms with 1 password field, part 2</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: forms with 1 password field, part 2 +<script> +gTestDependsOnDeprecatedLogin = true; +runChecksAfterCommonInit(() => startTest()); + +let DEFAULT_ORIGIN = window.location.origin; +</script> +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: simple form fill, part 2 **/ + +async function startTest() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <form id='form1' action='formtest.js'> 1 + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form2' action='formtest.js'> 2 + <input type='password' name='pname' value='' disabled> + <button type='submit'>Submit</button> + </form> + + <form id='form3' action='formtest.js'> 3 + <input type='password' name='pname' value='' readonly> + <button type='submit'>Submit</button> + </form> + + <form id='form4' action='formtest.js'> 4 + <input type='text' name='uname' value=''> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form5' action='formtest.js'> 5 + <input type='text' name='uname' value='' disabled> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form6' action='formtest.js'> 6 + <input type='text' name='uname' value='' readonly> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form7' action='formtest.js'> 7 + <input type='text' name='uname' value=''> + <input type='password' name='pname' value='' disabled> + <button type='submit'>Submit</button> + </form> + + <form id='form8' action='formtest.js'> 8 + <input type='text' name='uname' value=''> + <input type='password' name='pname' value='' readonly> + <button type='submit'>Submit</button> + </form> + + <form id='form9' action='formtest.js'> 9 + <input type='text' name='uname' value='TESTUSER'> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form10' action='formtest.js'> 10 + <input type='text' name='uname' value='TESTUSER' readonly> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form11' action='formtest.js'> 11 + <input type='text' name='uname' value='TESTUSER' disabled> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form>`, win, 11); + + var f; + + // Test various combinations of disabled/readonly inputs + await checkLoginFormInFrameWithElementValues(win, 1, "testpass"); // control + await checkUnmodifiedFormInFrame(win, 2); + await checkUnmodifiedFormInFrame(win, 3); + await checkLoginFormInFrameWithElementValues(win, 4, "testuser", "testpass"); // control + for (f = 5; f <= 8; f++) { + await checkUnmodifiedFormInFrame(win, f); + } + // Test case-insensitive comparison of username field + await checkLoginFormInFrameWithElementValues(win, 9, "testuser", "testpass"); + await checkLoginFormInFrameWithElementValues(win, 10, "TESTUSER", "testpass"); + await checkLoginFormInFrameWithElementValues(win, 11, "TESTUSER", "testpass"); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html new file mode 100644 index 0000000000..1d9224a819 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_1.html @@ -0,0 +1,190 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofill for forms with 2 password fields</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: forms with 2 password fields +<script> +gTestDependsOnDeprecatedLogin = true; +runChecksAfterCommonInit(() => startTest()); + +let DEFAULT_ORIGIN = window.location.origin; +</script> +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: simple form fill **/ + +async function startTest() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <!-- no username fields --> + + <form id='form1' action='formtest.js'> 1 + <!-- simple form, fill in first pw --> + <input type='password' name='pname' value=''> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form2' action='formtest.js'> 2 + <!-- same but reverse pname and qname, field names are ignored. --> + <input type='password' name='qname' value=''> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form3' action='formtest.js'> 3 + <!-- text field after password fields should be ignored, no username. --> + <input type='password' name='pname' value=''> + <input type='password' name='qname' value=''> + <input type='text' name='uname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form4' action='formtest.js'> 4 + <!-- nothing to do, password already present --> + <input type='password' name='pname' value='testpass'> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form5' action='formtest.js'> 5 + <!-- don't clobber an existing unrecognized password --> + <input type='password' name='pname' value='xxxxxxxx'> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form6' action='formtest.js'> 6 + <!-- fill in first field, 2nd field shouldn't be touched anyway. --> + <input type='password' name='pname' value=''> + <input type='password' name='qname' value='xxxxxxxx'> + <button type='submit'>Submit</button> + </form> + + + + <!-- with username fields --> + + + + <form id='form7' action='formtest.js'> 7 + <!-- simple form, should fill in username and first pw --> + <input type='text' name='uname' value=''> + <input type='password' name='pname' value=''> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form8' action='formtest.js'> 8 + <!-- reverse pname and qname, field names are ignored. --> + <input type='text' name='uname' value=''> + <input type='password' name='qname' value=''> + <input type='password' name='pname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form9' action='formtest.js'> 9 + <!-- username already filled, so just fill first password --> + <input type='text' name='uname' value='testuser'> + <input type='password' name='pname' value=''> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form10' action='formtest.js'> 10 + <!-- unknown username, don't fill in a password --> + <input type='text' name='uname' value='xxxxxxxx'> + <input type='password' name='pname' value=''> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form11' action='formtest.js'> 11 + <!-- don't clobber unknown password --> + <input type='text' name='uname' value='testuser'> + <input type='password' name='pname' value='xxxxxxxx'> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form12' action='formtest.js'> 12 + <!-- fill in 1st pass, don't clobber 2nd pass --> + <input type='text' name='uname' value='testuser'> + <input type='password' name='pname' value=''> + <input type='password' name='qname' value='xxxxxxxx'> + <button type='submit'>Submit</button> + </form> + + <form id='form13' action='formtest.js'> 13 + <!-- nothing to do, user and pass prefilled. life is easy. --> + <input type='text' name='uname' value='testuser'> + <input type='password' name='pname' value='testpass'> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form14' action='formtest.js'> 14 + <!-- shouldn't fill in username because 1st pw field is unknown. --> + <input type='text' name='uname' value=''> + <input type='password' name='pname' value='xxxxxxxx'> + <input type='password' name='qname' value='testpass'> + <button type='submit'>Submit</button> + </form> + + <form id='form15' action='formtest.js'> 15 + <!-- textfield in the middle of pw fields should be ignored --> + <input type='password' name='pname' value=''> + <input type='text' name='uname' value=''> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form> + + <form id='form16' action='formtest.js'> 16 + <!-- same, and don't clobber existing unknown password --> + <input type='password' name='pname' value='xxxxxxxx'> + <input type='text' name='uname' value=''> + <input type='password' name='qname' value=''> + <button type='submit'>Submit</button> + </form>`, win, 16); + + var f = 1; + + // 1-6 no username + await checkLoginFormInFrameWithElementValues(win, f++, "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "xxxxxxxx"); + + // 7-15 with username + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", "", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "xxxxxxxx", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", "xxxxxxxx"); + await checkLoginFormInFrameWithElementValues(win, f++, "testuser", "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "", "xxxxxxxx", "testpass"); + await checkLoginFormInFrameWithElementValues(win, f++, "testpass", "", ""); + await checkLoginFormInFrameWithElementValues(win, f++, "xxxxxxxx", "", ""); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html new file mode 100644 index 0000000000..a2c60e5964 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html @@ -0,0 +1,111 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for form fill with 2 password fields</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: form fill, 2 password fields +<p id="display"></p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: form fill, 2 password fields **/ + +/* + * If a form has two password fields, other things may be going on.... + * + * 1 - The user might be creating a new login (2nd field for typo checking) + * 2 - The user is changing a password (old and new password each have field) + * + * This test is for case #1. + */ + +var numSubmittedForms = 0; +var numStartingLogins = 0; + +function startTest() { + // Check for unfilled forms + is(getFormElementByName(1, "uname").value, "", "Checking username 1"); + is(getFormElementByName(1, "pword").value, "", "Checking password 1A"); + is(getFormElementByName(1, "qword").value, "", "Checking password 1B"); + + // Fill in the username and password fields, for account creation. + // Form 1 + SpecialPowers.wrap(getFormElementByName(1, "uname")).setUserInput("newuser1"); + SpecialPowers.wrap(getFormElementByName(1, "pword")).setUserInput("newpass1"); + SpecialPowers.wrap(getFormElementByName(1, "qword")).setUserInput("newpass1"); + + // eslint-disable-next-line no-unused-vars + var button = getFormSubmitButton(1); + + todo(false, "form submission disabled, can't auto-accept dialog yet"); + SimpleTest.finish(); +} + + +// Called by each form's onsubmit handler. +function checkSubmit(formNum) { + numSubmittedForms++; + + // End the test at the last form. + if (formNum == 999) { + is(numSubmittedForms, 999, "Ensuring all forms submitted for testing."); + + (async () => { + var numEndingLogins = await LoginManager.countLogins("", "", ""); + + ok(numEndingLogins > 0, "counting logins at end"); + is(numStartingLogins, numEndingLogins + 222, "counting logins at end"); + + SimpleTest.finish(); + })(); + return false; // return false to cancel current form submission + } + + // submit the next form. + var button = getFormSubmitButton(formNum + 1); + button.click(); + + return false; // return false to cancel current form submission +} + + +function getFormSubmitButton(formNum) { + var form = $("form" + formNum); // by id, not name + ok(form != null, "getting form " + formNum); + + // we can't just call form.submit(), because that doesn't seem to + // invoke the form onsubmit handler. + var button = form.firstChild; + while (button && button.type != "submit") { + button = button.nextSibling; + } + ok(button != null, "getting form submit button"); + + return button; +} + +runChecksAfterCommonInit(startTest); + +</script> +</pre> +<div id="content" style="display: none"> + <form id="form1" onsubmit="return checkSubmit(1)" action="http://newuser.com"> + <input type="text" name="uname"> + <input type="password" name="pword"> + <input type="password" name="qword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + +</div> + +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html new file mode 100644 index 0000000000..59aec20612 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_3pw_1.html @@ -0,0 +1,259 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofill for forms with 3 password fields</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: forms with 3 password fields (form filling) +<script> +gTestDependsOnDeprecatedLogin = true; +runChecksAfterCommonInit(() => startTest()); + +let DEFAULT_ORIGIN = window.location.origin; +</script> +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: form fill, 3 password fields **/ + +// Test to make sure 3-password forms are filled properly. + +async function startTest() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <p>The next three forms are <b>user/pass/passB/passC</b>, as all-empty, preuser(only), and preuser/pass</p> + <form id="form1" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + <input type="password" name="qword"> + <input type="password" name="rword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <form id="form2" action="formtest.js"> + <input type="text" name="uname" value="testuser"> + <input type="password" name="pword"> + <input type="password" name="qword"> + <input type="password" name="rword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <form id="form3" action="formtest.js"> + <input type="text" name="uname" value="testuser"> + <input type="password" name="pword" value="testpass"> + <input type="password" name="qword"> + <input type="password" name="rword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + + <p>The next three forms are <b>user/passB/pass/passC</b>, as all-empty, preuser(only), and preuser/pass</p> + <form id="form4" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="qword"> + <input type="password" name="pword"> + <input type="password" name="rword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <form id="form5" action="formtest.js"> + <input type="text" name="uname" value="testuser"> + <input type="password" name="qword"> + <input type="password" name="pword"> + <input type="password" name="rword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <form id="form6" action="formtest.js"> + <input type="text" name="uname" value="testuser"> + <input type="password" name="qword"> + <input type="password" name="pword" value="testpass"> + <input type="password" name="rword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <p>The next three forms are <b>user/passB/passC/pass</b>, as all-empty, preuser(only), and preuser/pass</p> + <form id="form7" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="qword"> + <input type="password" name="rword"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <form id="form8" action="formtest.js"> + <input type="text" name="uname" value="testuser"> + <input type="password" name="qword"> + <input type="password" name="rword"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <form id="form9" action="formtest.js"> + <input type="text" name="uname" value="testuser"> + <input type="password" name="qword"> + <input type="password" name="rword"> + <input type="password" name="pword" value="testpass"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form>`, win, 9); + + let TESTCASES = [ + // Check form 1 + { + formNum: 1, + isAssertionTuples: [ + ["uname", "testuser", "Checking username 1"], + ["pword", "testpass", "Checking password 1"], + ["qword", "", "Checking password 1 (q)"], + ["rword", "", "Checking password 1 (r)"], + ], + }, + // Check form 2 + { + formNum: 2, + isAssertionTuples: [ + ["uname", "testuser", "Checking username 2"], + ["pword", "testpass", "Checking password 2"], + ["qword", "", "Checking password 2 (q)"], + ["rword", "", "Checking password 2 (r)"], + ], + }, + // Check form 3 + { + formNum: 3, + isAssertionTuples: [ + ["uname", "testuser", "Checking username 3"], + ["pword", "testpass", "Checking password 3"], + ["qword", "", "Checking password 3 (q)"], + ["rword", "", "Checking password 3 (r)"], + ], + }, + // Check form 4 + { + formNum: 4, + isAssertionTuples: [ + ["uname", "testuser", "Checking username 4"], + ["rword", "", "Checking password 4 (r)"], + ], + todoIsAssertionTuples: [ + ["qword", "", "Checking password 4 (q)"], + ["pword", "testpass", "Checking password 4"], + ], + }, + // Check form 5 + { + formNum: 5, + isAssertionTuples: [ + ["uname", "testuser", "Checking username 5"], + ["rword", "", "Checking password 5 (r)"], + ], + todoIsAssertionTuples: [ + ["qword", "", "Checking password 5 (q)"], + ["pword", "testpass", "Checking password 5"], + ], + }, + // Check form 6 + { + formNum: 6, + isAssertionTuples: [ + ["uname", "testuser", "Checking username 6"], + ["pword", "testpass", "Checking password 6"], + ["rword", "", "Checking password 6 (r)"], + ], + todoIsAssertionTuples: [ + ["qword", "", "Checking password 6 (q)"], + ], + }, + // Check form 7 + { + formNum: 7, + isAssertionTuples: [ + ["uname", "testuser", "Checking username 7"], + ["rword", "", "Checking password 7 (r)"], + ], + todoIsAssertionTuples: [ + ["qword", "", "Checking password 7 (q)"], + ["pword", "testpass", "Checking password 7"], + ], + }, + // Check form 8 + { + formNum: 8, + isAssertionTuples: [ + ["uname", "testuser", "Checking username 8"], + ["rword", "", "Checking password 8 (r)"], + ], + todoIsAssertionTuples: [ + ["qword", "", "Checking password 8 (q)"], + ["pword", "testpass", "Checking password 8"], + ], + }, + // Check form 9 + { + formNum: 9, + isAssertionTuples: [ + ["uname", "testuser", "Checking username 9"], + ["rword", "", "Checking password 9 (r)"], + ["pword", "testpass", "Checking password 9"], + ], + todoIsAssertionTuples: [ + ["qword", "", "Checking password 9 (q)"], + ], + }, + ]; + + await SpecialPowers.spawn(win, [TESTCASES], (testcasesF) => { + let doc = this.content.document; + for (let testcase of testcasesF) { + let { formNum } = testcase; + for (let tuple of testcase.isAssertionTuples) { + let [name, value, message] = tuple; + is(doc.querySelector(`#form${formNum} input[name=${name}]`).value, value, message); + } + if (!testcase.todoIsAssertionTuples) { + continue; + } + // TODO: Bug 1669614 + // for (let tuple of testcase.todoIsAssertionTuples) { + // let [name, value, message] = tuple; + // todo_is(doc.querySelector(`#form${formNum} input[name=${name}]`).value, value, message); + // } + } + }); + + // TODO: as with the 2-password cases, add tests to check for creating new + // logins and changing passwords. + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_honor_autocomplete_off.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_honor_autocomplete_off.html new file mode 100644 index 0000000000..42214485c8 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_honor_autocomplete_off.html @@ -0,0 +1,149 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test login autofill autocomplete when signon.autofillForms.autocompleteOff is false</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: autofilling when autocomplete=off +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/** Test for Login Manager: multiple login autocomplete. **/ + +// Check for expected username/password in form. +function checkFormValues(form, expectedUsername, expectedPassword) { + let uname = form.querySelector("[name='uname']"); + let pword = form.querySelector("[name='pword']"); + is(uname.value, expectedUsername, `Checking ${form.id} username is: ${expectedUsername}`); + is(pword.value, expectedPassword, `Checking ${form.id} password is: ${expectedPassword}`); +} + +async function autoCompleteFieldsFromFirstMatch(form) { + // trigger autocomplete from the username field + await SimpleTest.promiseFocus(form.ownerGlobal); + let uname = form.querySelector("[name='uname']"); + await popupBy(() => uname.focus()); + + let formFilled = promiseFormsProcessedInSameProcess(); + await synthesizeKey("KEY_ArrowDown"); // open + await synthesizeKey("KEY_Enter"); + await formFilled; + await Promise.resolve(); +} + +add_setup(async () => { + // Set the pref before the document loads. + SpecialPowers.setBoolPref("signon.autofillForms.autocompleteOff", false); + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("signon.autofillForms.autocompleteOff"); + }); + + await setStoredLoginsAsync( + [window.location.origin, "https://autocomplete", null, "singleuser", "singlepass", "uname", "pword"] + ); + listenForUnexpectedPopupShown(); +}); + +/* Tests for autofill of single-user forms for when we honor autocomplete=off on password fields */ +add_task(async function honor_password_autocomplete_off() { + const form = createLoginForm({ + action: "https://autocomplete", + password: { + autocomplete: "off" + } + }); + await promiseFormsProcessedInSameProcess(); + await SimpleTest.promiseFocus(window); + // With the pref toggled off, and with autocomplete=off on the password field, + // we expect not to have autofilled this form + checkFormValues(form, "", ""); + // ..but it should autocomplete just fine + await autoCompleteFieldsFromFirstMatch(form); + checkFormValues(form, "singleuser", "singlepass"); +}); + +add_task(async function honor_username_autocomplete_off() { + const form = createLoginForm({ + action: "https://autocomplete", + username: { + autocomplete: "off" + } + }); + await promiseFormsProcessedInSameProcess(); + await SimpleTest.promiseFocus(window); + // With the pref toggled off, and with autocomplete=off on the username field, + // we expect to have autofilled this form + checkFormValues(form, "singleuser", "singlepass"); +}); + +add_task(async function honor_form_autocomplete_off() { + const form = createLoginForm({ + action: "https://autocomplete", + autocomplete: "off" + }); + await promiseFormsProcessedInSameProcess(); + await SimpleTest.promiseFocus(window); + // With the pref toggled off, and with autocomplete=off on the form, + // we expect to have autofilled this form + checkFormValues(form, "singleuser", "singlepass"); +}); + +add_task(async function honor_username_and_password_autocomplete_off() { + const form = createLoginForm({ + action: "https://autocomplete", + username: { + autocomplete: "off" + }, + password: { + autocomplete: "off" + } + }); + await promiseFormsProcessedInSameProcess(); + await SimpleTest.promiseFocus(window); + // With the pref toggled off, and autocomplete=off on the username and password field, + // we expect not to have autofilled this form + checkFormValues(form, "", ""); + // ..but it should autocomplete just fine + await autoCompleteFieldsFromFirstMatch(form); + checkFormValues(form, "singleuser", "singlepass"); +}); + +add_task(async function reference_form() { + const form = createLoginForm({ + action: "https://autocomplete" + }); + await promiseFormsProcessedInSameProcess(); + await SimpleTest.promiseFocus(window); + // (this is a control, w/o autocomplete=off, to ensure the login + // that was being suppressed would have been filled in otherwise) + checkFormValues(form, "singleuser", "singlepass"); +}); + +add_task(async function honor_username_autocomplete_off_without_password() { + const form = createLoginForm({ + action: "https://autocomplete", + username: { + id: "username", + autocomplete: "off" + }, + password: false + }); + await promiseFormsProcessedInSameProcess(); + await SimpleTest.promiseFocus(window); + // With the pref toggled off, and with autocomplete=off on the username field + // in a username-only form, we expect to have autofilled this form + is(form.uname.value, "singleuser", `Checking form6 username is: singleuser`); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html new file mode 100644 index 0000000000..12aeb0bab3 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_html5.html @@ -0,0 +1,165 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for html5 input types (email, tel, url, etc.)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: html5 input types (email, tel, url, etc.) +<script> +runChecksAfterCommonInit(() => startTest()); + +const DEFAULT_ORIGIN = window.location.origin; + +runInParent(async function setup() { + const login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + const login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + const login3 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + const login4 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + const login5 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + + login1.init("http://mochi.test:8888", "http://bug600551-1", null, + "testuser@example.com", "testpass1", "", ""); + login2.init("http://mochi.test:8888", "http://bug600551-2", null, + "555-555-5555", "testpass2", "", ""); + login3.init("http://mochi.test:8888", "http://bug600551-3", null, + "http://mozilla.org", "testpass3", "", ""); + login4.init("http://mochi.test:8888", "http://bug600551-4", null, + "123456789", "testpass4", "", ""); + login5.init("http://mochi.test:8888", "http://bug600551-5", null, + "test", "test", "", ""); + + await Services.logins.addLogins([ + login1, + login2, + login3, + login4, + login5, + ]); +}); +</script> + +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/* Test for Login Manager: 600551 + (Password manager not working with input type=email) + */ +async function startTest() { + const win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <form id="form1" action="http://bug600551-1"> + <input type="email" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form2" action="http://bug600551-2"> + <input type="tel" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form3" action="http://bug600551-3"> + <input type="url" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form4" action="http://bug600551-4"> + <input type="number" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form5" action="http://bug600551-5"> + <input type="search" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <!-- The following forms should not be filled with usernames --> + <form id="form6" action="formtest.js"> + <input type="datetime" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form7" action="formtest.js"> + <input type="date" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form8" action="formtest.js"> + <input type="month" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form9" action="formtest.js"> + <input type="week" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form10" action="formtest.js"> + <input type="time" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form11" action="formtest.js"> + <input type="datetime-local" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form12" action="formtest.js"> + <input type="range" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + + <form id="form13" action="formtest.js"> + <input type="color" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form>`, win, 13); + + await checkLoginFormInFrameWithElementValues(win, 1, "testuser@example.com", "testpass1"); + await checkLoginFormInFrameWithElementValues(win, 2, "555-555-5555", "testpass2"); + await checkLoginFormInFrameWithElementValues(win, 3, "http://mozilla.org", "testpass3"); + await checkLoginFormInFrameWithElementValues(win, 4, "123456789", "testpass4"); + await checkLoginFormInFrameWithElementValues(win, 5, "test", "test"); + + info("type=datetime should not be considered a username"); + await checkLoginFormInFrameWithElementValues(win, 6, ""); + info("type=date should not be considered a username"); + await checkLoginFormInFrameWithElementValues(win, 7, ""); + info("type=month should not be considered a username"); + await checkLoginFormInFrameWithElementValues(win, 8, ""); + info("type=week should not be considered a username"); + await checkLoginFormInFrameWithElementValues(win, 9, ""); + info("type=time should not be considered a username"); + await checkLoginFormInFrameWithElementValues(win, 10, ""); + info("type=datetime-local should not be considered a username"); + await checkLoginFormInFrameWithElementValues(win, 11, ""); + info("type=range should not be considered a username"); + await checkLoginFormInFrameWithElementValues(win, 12, "50"); + info("type=color should not be considered a username"); + await checkLoginFormInFrameWithElementValues(win, 13, "#000000"); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html new file mode 100644 index 0000000000..4a202a7c05 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwevent.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=355063 +--> +<head> + <meta charset="utf-8"/> + <title>Test for Bug 355063</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="application/javascript"> + /** Test for Bug 355063 **/ + gTestDependsOnDeprecatedLogin = true; + runChecksAfterCommonInit(async function startTest() { + info("startTest"); + // Password Manager's own listener should always have been added first, so + // the test's listener should be called after the pwmgr's listener fills in + // a login. + // + SpecialPowers.addChromeEventListener("DOMFormHasPassword", function eventFired() { + SpecialPowers.removeChromeEventListener("DOMFormHasPassword", eventFired); + var passField = $("p1"); + passField.addEventListener("input", checkForm); + }); + await setFormAndWaitForFieldFilled("<form id=form1>form1: <input id=u1><input type=password id=p1></form><br>", + {fieldSelector: "#u1", fieldValue: "testuser"}); + }); + + function checkForm() { + info("checkForm"); + var userField = document.getElementById("u1"); + var passField = document.getElementById("p1"); + is(userField.value, "testuser", "checking filled username"); + is(passField.value, "testpass", "checking filled password"); + + SimpleTest.finish(); + } +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=355063">Mozilla Bug 355063</a> +<p id="display"></p> +<div id="content"> +forms go here! +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html new file mode 100644 index 0000000000..ec9557d2c3 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_pwonly.html @@ -0,0 +1,193 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test forms and logins without a username</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: forms and logins without a username. +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +add_setup(async () => { + await setStoredLoginsAsync( + ["http://mochi.test:8888", "http://mochi.test:8888", null, "testuser", "testpass", "uname", "pword"], + ["http://mochi.test:8888", "http://mochi.test:1111", null, "", "1234"], + ["http://mochi.test:8888", "http://mochi.test:8888", null, "", "1234"] + ); +}); + +/** Test for Login Manager: password-only logins **/ +add_task(async function startTest() { + const win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(location.origin, ` + <!-- simple form: no username field, 1 password field --> + <form id='form1' action='http://mochi.test:1111/formtest.js'> 1 + <input type='password' name='pname' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- simple form: no username field, 2 password fields --> + <form id='form2' action='http://mochi.test:1111/formtest.js'> 2 + <input type='password' name='pname1' value=''> + <input type='password' name='pname2' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- simple form: no username field, 3 password fields --> + <form id='form3' action='http://mochi.test:1111/formtest.js'> 3 + <input type='password' name='pname1' value=''> + <input type='password' name='pname2' value=''> + <input type='password' name='pname3' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- simple form: no username field, 5 password fields --> + <form id='form4' action='http://mochi.test:1111/formtest.js'> 4 + <input type='password' name='pname1' value=''> + <input type='password' name='pname2' value=''> + <input type='password' name='pname3' value=''> + <input type='password' name='pname4' value=''> + <input type='password' name='pname5' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- 6 password fields, should be ignored. --> + <form id='form5' action='http://mochi.test:1111/formtest.js'> 5 + <input type='password' name='pname1' value=''> + <input type='password' name='pname2' value=''> + <input type='password' name='pname3' value=''> + <input type='password' name='pname4' value=''> + <input type='password' name='pname5' value=''> + <input type='password' name='pname6' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- 1 username field --> + <form id='form6' action='http://mochi.test:1111/formtest.js'> 6 + <input type='text' name='uname' value=''> + <input type='password' name='pname' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- 1 username field, with a value set --> + <form id='form7' action='http://mochi.test:1111/formtest.js'> 7 + <input type='text' name='uname' value='someuser'> + <input type='password' name='pname' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- + (The following forms have 2 potentially-matching logins, on is + password-only, the other is username+password) + --> + + <!-- 1 username field, with value set. Fill in the matching U+P login --> + <form id='form8' action='formtest.js'> 8 + <input type='text' name='uname' value='testuser'> + <input type='password' name='pname' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- 1 username field, with value set. Don't fill in U+P login--> + <form id='form9' action='formtest.js'> 9 + <input type='text' name='uname' value='someuser'> + <input type='password' name='pname' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- 1 username field, too small for U+P login --> + <form id='form10' action='formtest.js'> 10 + <input type='text' name='uname' value='' maxlength="4"> + <input type='password' name='pname' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- 1 username field, too small for U+P login --> + <form id='form11' action='formtest.js'> 11 + <input type='text' name='uname' value='' maxlength="0"> + <input type='password' name='pname' value=''> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- 1 username field, too small for U+P login --> + <form id='form12' action='formtest.js'> 12 + <input type='text' name='uname' value=''> + <input type='password' name='pname' value='' maxlength="4"> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- 1 username field, too small for either login --> + <form id='form13' action='formtest.js'> 13 + <input type='text' name='uname' value=''> + <input type='password' name='pname' value='' maxlength="1"> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form> + + <!-- 1 username field, too small for either login --> + <form id='form14' action='formtest.js'> 14 + <input type='text' name='uname' value=''> + <input type='password' name='pname' value='' maxlength="0"> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form>`, win, 14); + + await checkLoginFormInFrameWithElementValues(win, 1, "1234"); + await checkLoginFormInFrameWithElementValues(win, 2, "1234", ""); + await checkLoginFormInFrameWithElementValues(win, 3, "1234", "", ""); + await checkLoginFormInFrameWithElementValues(win, 4, "1234"); + await checkUnmodifiedFormInFrame(win, 5); + + await checkLoginFormInFrameWithElementValues(win, 6, "testuser", "testpass"); + await checkLoginFormInFrameWithElementValues(win, 7, "someuser", ""); + + await checkLoginFormInFrameWithElementValues(win, 8, "testuser", "testpass"); + await checkLoginFormInFrameWithElementValues(win, 9, "someuser", ""); + + await checkLoginFormInFrameWithElementValues(win, 10, "", "1234"); + await checkLoginFormInFrameWithElementValues(win, 11, "", "1234"); + await checkLoginFormInFrameWithElementValues(win, 12, "", "1234"); + + await checkUnmodifiedFormInFrame(win, 13); + await checkUnmodifiedFormInFrame(win, 14); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html new file mode 100644 index 0000000000..429ec2269c --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html @@ -0,0 +1,163 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test bug 627616 related to proxy authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + var Ci = SpecialPowers.Ci; + + function makeXHR(expectedStatus, expectedText, extra) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "authenticate.sjs?" + + "proxy_user=proxy_user&" + + "proxy_pass=proxy_pass&" + + "proxy_realm=proxy_realm&" + + "user=user1name&" + + "pass=user1pass&" + + "realm=mochirealm&" + + extra || ""); + xhr.onloadend = function() { + is(xhr.status, expectedStatus, "xhr.status"); + is(xhr.statusText, expectedText, "xhr.statusText"); + runNextTest(); + }; + return xhr; + } + + function testNonAnonymousCredentials() { + var xhr = makeXHR(200, "OK"); + xhr.send(); + } + + function testAnonymousCredentials() { + // Test that an anonymous request correctly performs proxy authentication + var xhr = makeXHR(401, "Authentication required"); + SpecialPowers.wrap(xhr).channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS; + xhr.send(); + } + + function testAnonymousNoAuth() { + // Next, test that an anonymous request still does not include any non-proxy + // authentication headers. + var xhr = makeXHR(200, "Authorization header not found", "anonymous=1"); + SpecialPowers.wrap(xhr).channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS; + xhr.send(); + } + + var gExpectedDialogs = 0; + var gCurrentTest; + function runNextTest() { + is(gExpectedDialogs, 0, "received expected number of auth dialogs"); + mm.sendAsyncMessage("prepareForNextTest"); + mm.addMessageListener("prepareForNextTestDone", function prepared(msg) { + mm.removeMessageListener("prepareForNextTestDone", prepared); + if (pendingTests.length) { + ({expectedDialogs: gExpectedDialogs, + test: gCurrentTest} = pendingTests.shift()); + gCurrentTest.call(this); + } else { + mm.sendAsyncMessage("cleanup"); + mm.addMessageListener("cleanupDone", () => { + // mm.destroy() is called as a cleanup function by runInParent(), no + // need to do it here. + SimpleTest.finish(); + }); + } + }); + } + + var pendingTests = [{expectedDialogs: 2, test: testNonAnonymousCredentials}, + {expectedDialogs: 1, test: testAnonymousCredentials}, + {expectedDialogs: 0, test: testAnonymousNoAuth}]; + + const mm = runInParent(() => { + const { classes: parentCc, interfaces: parentCi } = Components; + + const {NetUtil} = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" + ); + + const channel = NetUtil.newChannel({ + uri: "http://example.com", + loadUsingSystemPrincipal: true, + }); + + const pps = parentCc["@mozilla.org/network/protocol-proxy-service;1"]. + getService(parentCi.nsIProtocolProxyService); + pps.asyncResolve(channel, 0, { + async onProxyAvailable(req, uri, pi, status) { + const mozproxy = "moz-proxy://" + pi.host + ":" + pi.port; + const login1 = parentCc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(parentCi.nsILoginInfo); + login1.init(mozproxy, null, "proxy_realm", "proxy_user", "proxy_pass", + "", ""); + + const login2 = parentCc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(parentCi.nsILoginInfo); + login2.init("http://mochi.test:8888", null, "mochirealm", "user1name", + "user1pass", "", ""); + await Services.logins.addLogins([login1, login2]); + + sendAsyncMessage("setupDone"); + }, + QueryInterface: ChromeUtils.generateQI([parentCi.nsIProtocolProxyCallback]), + }); + + addMessageListener("prepareForNextTest", message => { + parentCc["@mozilla.org/network/http-auth-manager;1"]. + getService(parentCi.nsIHttpAuthManager). + clearAll(); + sendAsyncMessage("prepareForNextTestDone"); + }); + + const modalType = Services.prefs.getIntPref( + "prompts.modalType.httpAuth" + ); + const authPromptIsCommonDialog = + modalType === Services.prompt.MODAL_TYPE_WINDOW + || (modalType === Services.prompt.MODAL_TYPE_TAB + && Services.prefs.getBoolPref( + "prompts.tabChromePromptSubDialog", + false + )); + + const dialogObserverTopic = authPromptIsCommonDialog + ? "common-dialog-loaded" : "tabmodal-dialog-loaded"; + + function dialogObserver(subj, topic, data) { + if (authPromptIsCommonDialog) { + subj.Dialog.ui.prompt.document + .getElementById("commonDialog") + .acceptDialog(); + } else { + const prompt = subj.ownerGlobal.gBrowser.selectedBrowser + .tabModalPromptBox.getPrompt(subj); + prompt.Dialog.ui.button0.click(); // Accept button + } + sendAsyncMessage("promptAccepted"); + } + + Services.obs.addObserver(dialogObserver, dialogObserverTopic); + + addMessageListener("cleanup", message => { + Services.obs.removeObserver(dialogObserver, dialogObserverTopic); + sendAsyncMessage("cleanupDone"); + }); + }); + + mm.addMessageListener("promptAccepted", msg => { + gExpectedDialogs--; + }); + mm.addMessageListener("setupDone", msg => { + runNextTest(); + }); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html new file mode 100644 index 0000000000..f7de66a01d --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_776171.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=776171 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 776171 related to HTTP auth</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="startTest()"> +<script class="testbody" type="text/javascript"> + +/** + * This test checks we correctly ignore authentication entry + * for a subpath and use creds from the URL when provided when XHR + * is used with filled user name and password. + * + * 1. connect auth2/authenticate.sjs that expects user1:pass1 password + * 2. connect a dummy URL at the same path + * 3. connect authenticate.sjs that again expects user1:pass1 password + * in this case, however, we have an entry without an identity + * for this path (that is a parent for auth2 path in the first step) + */ + +SimpleTest.waitForExplicitFinish(); + +function doxhr(URL, user, pass, next) { + var xhr = new XMLHttpRequest(); + if (user && pass) { + xhr.open("POST", URL, true, user, pass); + } else { + xhr.open("POST", URL, true); + } + xhr.onload = function() { + is(xhr.status, 200, "Got status 200"); + next(); + }; + xhr.onerror = function() { + ok(false, "request passed"); + SimpleTest.finish(); + }; + xhr.send(); +} + +function startTest() { + doxhr("auth2/authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", function() { + doxhr("auth2", null, null, function() { + doxhr("authenticate.sjs?user=user1&pass=pass1&realm=realm1", "user1", "pass1", SimpleTest.finish); + }); + }); +} +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html new file mode 100644 index 0000000000..8a8499b584 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_case_differences.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test autocomplete due to multiple matching logins</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: autocomplete due to multiple matching logins +<p id="display"></p> +<!-- we presumably can't hide the content for this test. --> +<div id="content"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/** Test for Login Manager: autocomplete due to multiple matching logins **/ + +add_setup(async () => { + await setStoredLoginsAsync( + [location.origin, "https://autocomplete:8888", null, "name", "pass", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "Name", "Pass", "uname", "pword"], + [location.origin, "https://autocomplete:8888", null, "USER", "PASS", "uname", "pword"] + ); +}) + +add_task(async function test_empty_first_entry() { + const form = createLoginForm({ + action: "https://autocomplete:8888" + }); + + // Make sure initial form is empty. + checkLoginForm(form.uname, "", form.pword, ""); + + // Trigger autocomplete popup + form.uname.focus(); + + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is initially closed"); + const { items } = await openPopupOn(form.uname); + popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected"); + checkAutoCompleteResults(items, ["name", "Name", "USER"], "example.com", "initial"); + + // Check first entry + const index0Promise = notifySelectedIndex(0); + synthesizeKey("KEY_ArrowDown"); + await index0Promise; + checkLoginForm(form.uname, "", form.pword, ""); // value shouldn't update + synthesizeKey("KEY_Enter"); + await promiseFormsProcessedInSameProcess(); + checkLoginForm(form.uname, "name", form.pword, "pass"); +}); + +add_task(async function test_empty_second_entry() { + const form = createLoginForm({ + action: "https://autocomplete:8888" + }); + + await openPopupOn(form.uname); + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowDown"); // second + synthesizeKey("KEY_Enter"); + await promiseFormsProcessedInSameProcess(); + checkLoginForm(form.uname, "Name", form.pword, "Pass"); +}); + +add_task(async function test_empty_third_entry() { + const form = createLoginForm({ + action: "https://autocomplete:8888" + }); + + await openPopupOn(form.uname); + synthesizeKey("KEY_ArrowDown"); // first + synthesizeKey("KEY_ArrowDown"); // second + synthesizeKey("KEY_ArrowDown"); // third + synthesizeKey("KEY_Enter"); + await promiseFormsProcessedInSameProcess(); + checkLoginForm(form.uname, "USER", form.pword, "PASS"); +}); + +add_task(async function test_preserve_matching_username_case() { + const form = createLoginForm({ + action: "https://autocomplete:8888" + }); + await promiseFormsProcessedInSameProcess(); + + await openPopupOn(form.uname, { inputValue: "user" }); + // Check that we don't clobber user-entered text when tabbing away + // (even with no autocomplete entry selected) + synthesizeKey("KEY_Tab"); + await promiseFormsProcessedInSameProcess(); + checkLoginForm(form.uname, "user", form.pword, "PASS"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_dismissed_doorhanger_in_shadow_DOM.html b/toolkit/components/passwordmgr/test/mochitest/test_dismissed_doorhanger_in_shadow_DOM.html new file mode 100644 index 0000000000..87638b1132 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_dismissed_doorhanger_in_shadow_DOM.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test the password manager dismissed doorhanger can detect username and password fields in a Shadow DOM.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<iframe></iframe> + +<script type="application/javascript"> +add_setup(async () => { + const readyPromise = registerRunTests(); + info("Waiting for setup and page load"); + await readyPromise; + + // assert that there are no logins + const allLogins = await LoginManager.getAllLogins(); + is(allLogins.length, 0, "There are no logins"); +}); + +const IFRAME = document.querySelector("iframe"); +const PASSWORD_VALUE = "!@$*"; +const TESTCASES = [ + // Check that the Shadow DOM version of form_basic.html works + { + name: "test_form_each_field_in_its_own_shadow_root", + filename: "form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html", + }, + // Check that the Shadow DOM version of formless_basic.html works + { + name: "test_formless_each_field_in_its_own_shadow_root", + filename: "formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html", + }, + // Check that the nested Shadow DOM version of form_basic.html works + { + name: "test_form_nested_each_field_in_its_own_shadow_root", + filename: "form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html", + } +]; + +async function editPasswordFieldInShadowDOM() { + info("Editing the input field in the form with a Shadow DOM"); + await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [PASSWORD_VALUE], function(val) { + const doc = this.content.document; + // Grab the wrapper element to get the shadow root containing the password field + let wrapper = doc.getElementById("wrapper-password"); + if (!wrapper) { + // This is a nested Shadow DOM test case + const outerWrapper = doc.getElementById("outer-wrapper-password"); + const outerShadowRoot = outerWrapper.openOrClosedShadowRoot; + wrapper = outerShadowRoot.querySelector("#wrapper-password"); + } + // If the ShadowRoot's mode is "closed", it can only be accessed from a chrome-privileged + // (Bug 1421568) or addon context (Bug 1439153) + const shadowRoot = wrapper.openOrClosedShadowRoot; + const passwordField = shadowRoot.querySelector("[name='password']"); + Assert.equal(passwordField.value, "", "Check password didn't get autofilled"); + passwordField.setUserInput(val); + Assert.equal(passwordField.value, val, "Checking for filled password"); + } + ); +} + +async function testForm(testcase) { + const iframeLoaded = new Promise(resolve => { + IFRAME.addEventListener( + "load", + function(e) { + resolve(true); + }, + { once: true } + ); + }); + + // This could complete before the page finishes loading. + const formsProcessed = promiseFormsProcessed(); + + IFRAME.src = testcase.filename; + info("Waiting for test page to load in the iframe"); + await iframeLoaded; + + info("Waiting for 'input' event listener to be added to the form before editing"); + await formsProcessed; + + const passwordEditProcessed = getPasswordEditedMessage(); + + await editPasswordFieldInShadowDOM(); + + info("Waiting for parent process to receive input field edit message from content"); + await passwordEditProcessed; +} + +for (let testcase of TESTCASES) { + const taskName = testcase.name; + const tmp = { + async [taskName]() { + await testForm(testcase); + } + } + add_task(tmp[taskName]); +} +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formLike_rootElement_with_Shadow_DOM.html b/toolkit/components/passwordmgr/test/mochitest/test_formLike_rootElement_with_Shadow_DOM.html new file mode 100644 index 0000000000..06458893ea --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_formLike_rootElement_with_Shadow_DOM.html @@ -0,0 +1,151 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test that FormLike.rootElement points to right element when the page has Shadow DOM</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<iframe></iframe> + +<script type="application/javascript"> +const { LoginFormFactory } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/LoginFormFactory.sys.mjs" +); + +add_setup(async () => { + const readyPromise = registerRunTests(); + info("Waiting for setup and page load"); + await readyPromise; + + // assert that there are no logins + const allLogins = await LoginManager.getAllLogins(); + is(allLogins.length, 0, "There are no logins"); +}); + +const IFRAME = document.querySelector("iframe"); +const TESTCASES = [ + // Check that the Shadow DOM version of form_basic.html works + { + name: "test_form_each_field_in_its_own_shadow_root", + filename: "form_basic_shadow_DOM_each_field_in_its_own_shadow_root.html", + hostAndRootElementSelectorTuples: [["span#wrapper-password", "form"]], + }, + { + name: "test_form_both_fields_together_in_a_shadow_root", + filename: "form_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html", + hostAndRootElementSelectorTuples: [["span#wrapper-un-and-pw", "form"]], + }, + { + name: "test_form_form_and_fields_together_in_a_shadow_root", + filename: "form_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html", + hostAndRootElementSelectorTuples: [["span#wrapper", "form"]], + }, + // Check that the Shadow DOM version of formless_basic.html works + { + name: "test_formless_each_field_in_its_own_shadow_root", + filename: "formless_basic_shadow_DOM_each_field_in_its_own_shadow_root.html", + hostAndRootElementSelectorTuples: [["span#wrapper-password", "html"]], + }, + { + name: "test_formless_both_fields_together_in_a_shadow_root", + filename: "formless_basic_shadow_DOM_both_fields_together_in_a_shadow_root.html", + hostAndRootElementSelectorTuples: [["span#wrapper-un-and-pw", "html"]], + }, + { + name: "test_formless_form_and_fields_together_in_a_shadow_root.html", + filename: "formless_basic_shadow_DOM_form_and_fields_together_in_a_shadow_root.html", + hostAndRootElementSelectorTuples: [["span#wrapper", "html"]], + }, + // Check that the nested Shadow DOM version of form_basic.html works + { + name: "test_form_nested_each_field_in_its_own_shadow_root", + filename: "form_nested_shadow_DOM_each_field_in_its_own_shadow_root.html", + hostAndRootElementSelectorTuples: [["span#wrapper-password", "form"]], + outerHostElementSelector: "span#outer-wrapper-password", + }, + { + name: "test_form_nested_both_fields_together_in_a_shadow_root", + filename: "form_nested_shadow_DOM_both_fields_together_in_a_shadow_root.html", + hostAndRootElementSelectorTuples: [["span#inner-wrapper", "form"]], + outerHostElementSelector: "span#outer-wrapper", + }, + { + name: "test_form_nested_form_and_fields_together_in_a_shadow_root", + filename: "form_nested_shadow_DOM_form_and_fields_together_in_a_shadow_root.html", + hostAndRootElementSelectorTuples: [["span#inner-wrapper", "form"]], + outerHostElementSelector: "span#outer-wrapper", + }, + { + name: "test_multiple_forms_shadow_DOM_all_known_variants", + filename: "multiple_forms_shadow_DOM_all_known_variants.html", + hostAndRootElementSelectorTuples: [ + ["span#outer-wrapper", "html"], + ["span#wrapper-password-form-case-1", "form#form-case-1"], + ["span#wrapper-form-case-2", "form#form-case-2"], + ["span#wrapper-form-case-3", "form#form-case-3"], + ], + outerHostElementSelector: "span#outer-wrapper", + } +]; + +async function testForm(testcase) { + const iframeLoaded = new Promise(resolve => { + IFRAME.addEventListener( + "load", + function(e) { + resolve(true); + }, + { once: true } + ); + }); + + // This could complete before the page finishes loading. + const numForms = testcase.hostAndRootElementSelectorTuples.length; + const formsProcessed = promiseFormsProcessedInSameProcess(numForms); + + IFRAME.src = testcase.filename; + info("Waiting for test page to load in the iframe"); + await iframeLoaded; + + info(`Wait for ${numForms} form(s) to be processed.`); + await formsProcessed; + + const iframeDoc = SpecialPowers.wrap(IFRAME.contentWindow).document; + for (let [hostElementSelector, rootElementSelector] of testcase.hostAndRootElementSelectorTuples) { + info("Get the expected rootElement from the document"); + let hostElement = iframeDoc.querySelector(hostElementSelector); + let outerShadowRoot = null; + if (!hostElement) { + // Nested Shadow DOM testcase + const outerHostElement = iframeDoc.querySelector(testcase.outerHostElementSelector); + outerShadowRoot = outerHostElement.openOrClosedShadowRoot; + hostElement = outerShadowRoot.querySelector(hostElementSelector); + } + const shadowRoot = hostElement.openOrClosedShadowRoot; + let expectedRootElement = iframeDoc.querySelector(rootElementSelector); + if (!expectedRootElement) { + // The form itself is inside a ShadowRoot and/or there is a ShadowRoot in between the field and form + expectedRootElement = + shadowRoot.querySelector(rootElementSelector) || + outerShadowRoot.querySelector(rootElementSelector); + } + ok(LoginFormFactory.getRootElementsWeakSetForDocument(iframeDoc).has(expectedRootElement), "Ensure formLike.rootElement has the expected value"); + } +} + +for (let testcase of TESTCASES) { + const taskName = testcase.name; + const tmp = { + async [taskName]() { + await testForm(testcase); + } + } + add_task(tmp[taskName]); +} +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html new file mode 100644 index 0000000000..21f5f18904 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_1.html @@ -0,0 +1,140 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for considering form action</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: Bug 360493 +<script> +gTestDependsOnDeprecatedLogin = true; +runChecksAfterCommonInit(() => startTest()); + +let DEFAULT_ORIGIN = window.location.origin; +</script> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: 360493 (Cross-Site Forms + Password + Manager = Security Failure) **/ + +// This test is designed to make sure variations on the form's |action| +// and |method| continue to work with the fix for 360493. + +async function startTest() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <!-- normal form with normal relative action. --> + <form id="form1" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- fully specify the action URL --> + <form id="form2" action="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- fully specify the action URL, and change the path --> + <form id="form3" action="http://mochi.test:8888/zomg/wtf/bbq/passwordmgr/test/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- fully specify the action URL, and change the path and filename --> + <form id="form4" action="http://mochi.test:8888/zomg/wtf/bbq/passwordmgr/test/not_a_test.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- specify the action URL relative to the current document--> + <form id="form5" action="./formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- specify the action URL relative to the current server --> + <form id="form6" action="/tests/toolkit/components/passwordmgr/test/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Change the method from get to post --> + <form id="form7" action="formtest.js" method="POST"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Blank action URL specified --> + <form id="form8" action=""> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- |action| attribute entirely missing --> + <form id="form9" > + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- action url as javascript --> + <form id="form10" action="javascript:alert('this form is not submitted so this alert should not be invoked');"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form>`, win, 10); + + // TODO: action=IP.ADDRESS instead of HOSTNAME? + // TODO: test with |base href="http://othersite//"| ? + + for (var i = 1; i <= 9; i++) { + // Check form i + await checkLoginFormInFrameWithElementValues(win, i, "testuser", "testpass"); + } + + // The login's formActionOrigin isn't "javascript:", so don't fill it in. + await checkLoginFormInFrameWithElementValues(win, 10, "", ""); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html new file mode 100644 index 0000000000..2eae0958ca --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_2.html @@ -0,0 +1,173 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for considering form action</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: Bug 360493 +<script> +gTestDependsOnDeprecatedLogin = true; +runChecksAfterCommonInit(() => startTest()); + +let DEFAULT_ORIGIN = window.location.origin; +</script> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: 360493 (Cross-Site Forms + Password Manager = Security Failure) **/ + +async function startTest() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <!-- The tests in this page exercise things that shouldn't work. --> + + <!-- Change port # of action URL from 8888 to 7777 --> + <form id="form1" action="http://localhost:7777/tests/toolkit/components/passwordmgr/test/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- No port # in action URL --> + <form id="form2" action="http://localhost/tests/toolkit/components/passwordmgr/test/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Change protocol from http:// to ftp://, include the expected 8888 port # --> + <form id="form3" action="ftp://localhost:8888/tests/toolkit/components/passwordmgr/test/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Change protocol from http:// to ftp://, no port # specified --> + <form id="form4" action="ftp://localhost/tests/toolkit/components/passwordmgr/test/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Try a weird URL. --> + <form id="form5" action="about:blank"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Try a weird URL. (If the normal embedded action URL doesn't work, that should mean other URLs won't either) --> + <form id="form6" action="view-source:http://localhost:8888/tests/toolkit/components/passwordmgr/test/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Try a weird URL. --> + <form id="form7" action="view-source:formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Action URL points to a different host (this is the archetypical exploit) --> + <form id="form8" action="http://www.cnn.com/"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Action URL points to a different host, user field prefilled --> + <form id="form9" action="http://www.cnn.com/"> + <input type="text" name="uname" value="testuser"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- Try wrapping a evil form around a good form, to see if we can confuse the parser. --> + <form id="form10-A" action="http://www.cnn.com/"> + <form id="form10-B" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit (inner)</button> + <button type="reset"> Reset (inner)</button> + </form> + <button type="submit" id="neutered_submit10">Submit (outer)</button> + <button type="reset">Reset (outer)</button> + </form> + + <!-- Try wrapping a good form around an evil form, to see if we can confuse the parser. --> + <form id="form11-A" action="formtest.js"> + <form id="form11-B" action="http://www.cnn.com/"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit (inner)</button> + <button type="reset"> Reset (inner)</button> + </form> + <button type="submit" id="neutered_submit11">Submit (outer)</button> + <button type="reset">Reset (outer)</button> + </form>`, win, 11); + + // TODO: probably should have some accounts which have no port # in the action url. + // JS too. And different host/proto. + // TODO: www.site.com vs. site.com? + // TODO: foo.site.com vs. bar.site.com? + + for (var i = 1; i <= 8; i++) { + // Check form i + await checkLoginFormInFrameWithElementValues(win, i, "", ""); + } + + await checkLoginFormInFrameWithElementValues(win, 9, "testuser", ""); + + await checkLoginFormInFrameWithElementValues(win, "10-A", "", ""); + + // The DOM indicates this form could be filled, as the evil inner form + // is discarded. And yet pwmgr seems not to fill it. Not sure why. + todo(false, "Mangled form combo not being filled when maybe it could be?"); + await checkLoginFormInFrameWithElementValues(win, "11-A", "testuser", "testpass"); + + // Verify this by making sure there are no extra forms in the document, and + // that the submit button for the neutered forms don't do anything. + // If the test finds extra forms the submit() causes the test to timeout, then + // there may be a security issue. + await SpecialPowers.spawn(win, [], async function submitForms() { + is(this.content.document.forms.length, 11, "Checking for unexpected forms"); + this.content.document.getElementById("neutered_submit10").click(); + this.content.document.getElementById("neutered_submit11").click(); + }); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html new file mode 100644 index 0000000000..717805f06a --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_form_action_javascript.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test forms with a JS submit action</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: form with JS submit action +<script> + +add_setup(async () => { + await setStoredLoginsAsync( + [location.origin, "javascript:", null, "jsuser", "jspass123", "uname", "pword"] + ); +}); + +/** Test for Login Manager: JS action URL **/ +add_task(async function startTest() { + const win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(location.origin, ` + <form id='form1' action='javascript:alert("never shows")'> 1 + <input name="uname"> + <input name="pword" type="password"> + + <button type='submit'>Submit</button> + <button type='reset'> Reset </button> + </form>`, win); + + await checkLoginFormInFrameWithElementValues(win, 1, "jsuser", "jspass123"); +}); +</script> + +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html new file mode 100644 index 0000000000..4d1b7582a9 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_autofill.html @@ -0,0 +1,140 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test autofilling of fields outside of a form</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> +gTestDependsOnDeprecatedLogin = true; +let doneSetupPromise = new Promise(resolve => { + document.addEventListener("DOMContentLoaded", () => { + document.getElementById("loginFrame").addEventListener("load", async evt => { + // Tell the parent to setup test logins. + await runChecksAfterCommonInit(); + resolve(); + }); + }); +}); + +add_setup(async () => { + info("Waiting for loads and setup"); + await doneSetupPromise; + + await loadRecipes({ + siteRecipes: [{ + hosts: ["mochi.test:8888"], + usernameSelector: "input[name='recipeuname']", + passwordSelector: "input[name='recipepword']", + }], + }); +}); + + +const DEFAULT_ORIGIN = "http://mochi.test:8888"; +const TESTCASES = [ + { + // Inputs + document: `<input type=password>`, + + // Expected outputs + expectedInputValues: ["testpass"], + }, + { + document: `<input> + <input type=password>`, + expectedInputValues: ["testuser", "testpass"], + }, + { + document: `<input> + <input type=password> + <input type=password>`, + expectedInputValues: ["testuser", "testpass", ""], + }, + { + document: `<input> + <input type=password> + <input type=password> + <input type=password>`, + expectedInputValues: ["testuser", "testpass", "", ""], + }, + { + document: `<input> + <input type=password form="form1"> + <input type=password> + <form id="form1"> + <input> + <input type=password> + </form>`, + expectedFormCount: 2, + expectedInputValues: ["testuser", "testpass", "testpass", "", ""], + }, + { + document: `<!-- formless password field selector recipe test --> + <input> + <input type=password> + <input> + <input type=password name="recipepword">`, + expectedInputValues: ["", "", "testuser", "testpass"], + }, + { + document: `<!-- formless username and password field selector recipe test --> + <input name="recipeuname"> + <input> + <input type=password> + <input type=password name="recipepword">`, + expectedInputValues: ["testuser", "", "", "testpass"], + }, + { + document: `<!-- form and formless recipe field selector test --> + <input name="recipeuname"> + <input> + <input type=password form="form1"> <!-- not filled since recipe affects both FormLikes --> + <input type=password> + <input type=password name="recipepword"> + <form id="form1"> + <input> + <input type=password> + </form>`, + expectedFormCount: 2, + expectedInputValues: ["testuser", "", "", "", "testpass", "", ""], + }, +]; + +add_task(async function test() { + let loginFrame = document.getElementById("loginFrame"); + let frameDoc = loginFrame.contentWindow.document; + + for (let tc of TESTCASES) { + info("Starting testcase: " + JSON.stringify(tc)); + + let numFormLikesExpected = tc.expectedFormCount || 1; + + let processedFormPromise = promiseFormsProcessedInSameProcess(numFormLikesExpected); + + frameDoc.documentElement.innerHTML = tc.document; + info("waiting for " + numFormLikesExpected + " processed form(s)"); + await processedFormPromise; + + let testInputs = frameDoc.documentElement.querySelectorAll("input"); + is(testInputs.length, tc.expectedInputValues.length, "Check number of inputs"); + for (let i = 0; i < tc.expectedInputValues.length; i++) { + let expectedValue = tc.expectedInputValues[i]; + is(testInputs[i].value, expectedValue, `Check expected input value ${i} : ${expectedValue}`); + } + } +}); + +</script> + +<p id="display"></p> + +<div id="content"> + <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/blank.html"></iframe> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html new file mode 100644 index 0000000000..4e98fd1b9a --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html @@ -0,0 +1,242 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test capturing of fields outside of a form</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> +const { LoginFormFactory } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/LoginFormFactory.sys.mjs" +); +const { LoginManagerChild } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/LoginManagerChild.sys.mjs" +); + +function loadFrame() { + return new Promise(resolve => { + document.getElementById("loginFrame").addEventListener("load", (evt) => { + if (evt.target.contentWindow.location.href.includes("blank.html")) { + resolve(); + } + }); + }); +} + +let loadPromise = new Promise(resolve => { + document.addEventListener("DOMContentLoaded", () => { + resolve(loadFrame()); + }); +}); + +add_setup(async () => { + info("Waiting for page and frame loads"); + await loadPromise; + + await loadRecipes({ + siteRecipes: [{ + hosts: ["mochi.test:8888"], + usernameSelector: "input[name='recipeuname']", + passwordSelector: "input[name='recipepword']", + }], + }); +}); + +const DEFAULT_ORIGIN = "http://mochi.test:8888"; +const TESTCASES = [ + { + // Inputs + document: `<input type=password value="">`, + selectorValues: { + "[type=password]": "pass1", + }, + inputIndexForFormLike: 0, + expectedFormsCount: 1, + + // Expected outputs similar to PasswordManager:onFormSubmit + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: null, + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + }, + { + document: `<input id="u1" value=""> + <input type=password value="">`, + selectorValues: { + "#u1": "user1", + "[type=password]": "pass1", + }, + inputIndexForFormLike: 0, + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + }, + { + document: `<input id="u1" value=""> + <input type=password value="">`, + selectorValues: { + "#u1": "user1", + "[type=password]": "pass1", + }, + inputIndexForFormLike: 1, + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + }, + { + document: `<input id="u1" value=""> + <input id="p1" type=password value=""> + <input id="p2" type=password value="">`, + selectorValues: { + "#u1": "user1", + "#p1": "pass1", + "#p2": "pass2", + }, + inputIndexForFormLike: 2, + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: "pass1", + }, + { + document: `<input id="u1" value=""> + <input id="p1" type=password value=""> + <input id="p2" type=password value=""> + <input id="p3" type=password value="">`, + selectorValues: { + "#u1": "user1", + "#p1": "pass1", + "#p2": "pass2", + "#p3": "pass2", + }, + inputIndexForFormLike: 3, + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: "pass1", + }, + { + document: `<input id="u1" value=""> + <input id="p1" type=password value="" form="form1"> + <input id="p2" type=password value=""> + <form id="form1"> + <input id="u2" value=""> + <input id="p3" type=password value=""> + </form>`, + selectorValues: { + "#u1": "user1", + "#p1": "user2", + "#p2": "pass1", + "#u2": "user3", + "#p3": "pass2", + }, + inputIndexForFormLike: 2, + expectedFormsCount: 2, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + }, + { + document: `<!-- recipe field override --> + <input name="recipeuname" value=""> + <input id="u1" value=""> + <input id="p1" type=password value=""> + <input name="recipepword" type=password value="">`, + selectorValues: { + "[name='recipeuname']": "username from recipe", + "#u1": "default field username", + "#p1": "pass1", + "[name='recipepword']": "pass2", + }, + inputIndexForFormLike: 2, + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "username from recipe", + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: null, + }, +]; + +let count = 0; +async function testFormlessSubmit(tc) { + let loginFrame = document.getElementById("loginFrame"); + + let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document; + info("Starting testcase: " + JSON.stringify(tc)); + + let formsProcessed = promiseFormsProcessedInSameProcess(tc.expectedFormsCount); + frameDoc.documentElement.innerHTML = tc.document; + await formsProcessed; + // We eliminate no user input as a reason for not capturing by modifying the value + setUserInputValues(frameDoc.documentElement, tc.selectorValues); + + let inputForFormLike = frameDoc.querySelectorAll("input")[tc.inputIndexForFormLike]; + + let formLike = LoginFormFactory.createFromField(inputForFormLike); + + info("Calling _onFormSubmit with FormLike"); + let submitProcessed = getSubmitMessage(); + LoginManagerChild.forWindow(frameDoc.defaultView)._onFormSubmit(formLike); + + let { origin, data } = await submitProcessed; + + // Check data sent via PasswordManager:onFormSubmit + is(origin, tc.origin, "Check origin"); + is(data.formActionOrigin, tc.formActionOrigin, "Check formActionOrigin"); + + if (tc.usernameFieldValue === null) { + is(data.usernameField, tc.usernameFieldValue, "Check usernameField"); + } else { + is(data.usernameField.value, tc.usernameFieldValue, "Check usernameField"); + } + + is(data.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue"); + + if (tc.oldPasswordFieldValue === null) { + is(data.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue"); + } else { + is(data.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue"); + } + + loadPromise = loadFrame(); + loginFrame.contentWindow.location = + "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html?" + count++; + await loadPromise; +} + +for (let tc of TESTCASES) { + let taskName = "testcase-" + count++; + let tmp = { + async [taskName]() { + await testFormlessSubmit(tc); + }, + }; + add_task(tmp[taskName]); +} +</script> + +<p id="display"></p> + +<div id="content"> + <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html new file mode 100644 index 0000000000..5512c57db2 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal.html @@ -0,0 +1,287 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test capturing of fields due to form removal</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> +let loadPromise = new Promise(resolve => { + document.addEventListener("DOMContentLoaded", () => { + document.getElementById("loginFrame").addEventListener("load", (evt) => { + resolve(); + }); + }); +}); + +add_setup(async () => { + info("Waiting for page and frame loads"); + await loadPromise; + + await loadRecipes({ + siteRecipes: [{ + hosts: ["test1.mochi.test:8888"], + usernameSelector: "input[name='recipeuname']", + passwordSelector: "input[name='recipepword']", + }], + }); +}); + +const DEFAULT_ORIGIN = "http://test1.mochi.test:8888"; +const SCRIPTS = { + // Test removing the form(or form-less password field), their parent node, and the top-level node. + REMOVE_FORM: `let e = document.querySelector("form"); e.parentNode.removeChild(e);`, + REMOVE_PASSWORD: `let e = document.querySelector("[type=password]"); e.parentNode.removeChild(e);`, + REMOVE_FORM_PARENT: `let e = document.querySelector("form").parentNode; e.parentNode.removeChild(e);`, + REMOVE_PASSWORD_PARENT: `let e = document.querySelector("[type=password]").parentNode; e.parentNode.removeChild(e);`, + REMOVE_TOP: `let e = document.querySelector("html"); e.parentNode.removeChild(e);`, + + // Add testcases related to page navigation here to ensure these cases still work + // when we have set up form removal observer. + PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`, + WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`, +}; +const TESTCASES = [ + { + // Inputs + document: `<input type=password value="">`, + selectorValues: { + "[type=password]": "pass1", + }, + expectedFormsCount: 1, + + // Expected outputs similar to PasswordManager:onFormSubmit + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: null, + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + }, + { + document: `<input id="u1" value=""> + <input type=password value="">`, + selectorValues: { + "#u1": "user1", + "[type=password]": "pass1", + }, + + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + }, + { + document: `<input id="u1" value=""> + <input id="p1" type=password value=""> + <input id="p2" type=password value="">`, + selectorValues: { + "#u1": "user1", + "#p1": "pass1", + "#p2": "pass2", + }, + + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: "pass1", + }, + { + document: `<input id="u1" value=""> + <input id="p1" type=password value=""> + <input id="p2" type=password value=""> + <input id="p3" type=password value="">`, + selectorValues: { + "#u1": "user1", + "#p1": "pass1", + "#p2": "pass2", + "#p3": "pass2", + }, + + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: "pass1", + }, + { + // Since there are two FormLikes to auto-submit in this case we mark + // one FormLike's password fields with a magic "ignore-form-submission" + // value so we can just focus on the other form. We then repeat the testcase + // below with the other FormLike ignored. + document: `<input id="u1" value=""> + <input type=password id="p1" value="" form="form1"> + <input type=password id="p2" value=""> + <form id="form1"> + <input id="u2" value=""> + <input id="p3" type=password value=""> + </form>`, + selectorValues: { + "#u1": "user1", + "#p1": "ignore-form-submission", + "#p2": "pass1", + "#u2": "user3", + "#p3": "ignore-form-submission", + }, + expectedFormsCount: 2, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + removePassword: true, + }, + { // Same as above but with the other form ignored. + document: `<input id="u1" value=""> + <input id="p1" type=password value="" form="form1"> + <input id="p2" type=password value=""> + <form id="form1"> + <input id="u2" value=""> + <input id="p3" type=password value=""> + </form>`, + selectorValues: { + "#u1": "user1", + "#p1": "pass2", + "#p2": "ignore-form-submission", + "#u2": "user3", + "#p3": "pass2", + }, + + expectedFormsCount: 2, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: null, + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: null, + }, + /* + XXX - Bug 1698498 : + This test case fails because when we call querySelector in LoginRecipes.jsm + after the form is removed, querySelector can't find the element. + { + document: `<!-- recipe field override --> + <input name="recipeuname" value=""> + <input id="u1" value=""> + <input id="p1" type=password value=""> + <input name="recipepword" type=password value="">`, + selectorValues: { + "[name='recipeuname']": "username from recipe", + "#u1": "default field username", + "#p1": "pass1", + "[name='recipepword']": "pass2", + }, + + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "username from recipe", + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: null, + },*/ +]; + +function filterFormSubmissions({ origin, data }) { + return data.newPasswordField.value != "ignore-form-submission"; +} + +async function testFormlesSubmitFormRemoval(tc, testDoc, scriptName) { + let loginFrame = document.getElementById("loginFrame"); + let loadedPromise = new Promise((resolve) => { + loginFrame.addEventListener("load", function() { + resolve(); + }, {once: true}); + }); + loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"; + await loadedPromise; + + let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document; + + let formsProcessed = promiseFormsProcessed(tc.expectedFormsCount); + frameDoc.documentElement.innerHTML = testDoc; + await formsProcessed; + // We eliminate no user input as a reason for not capturing by modifying the value + setUserInputValues(frameDoc.documentElement, tc.selectorValues) + + await SpecialPowers.spawn(frameDoc.defaultView, [], async () => { + await content.fetch("http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"); + }); + + let submitProcessed = getSubmitMessage(filterFormSubmissions); + info("Running " + scriptName + " script to cause a submission"); + frameDoc.defaultView.eval(SCRIPTS[scriptName]); + + info("Waiting for formSubmissionProcsssed message"); + let { origin, data } = await submitProcessed; + info("Got for formSubmissionProcsssed message"); + + // Check data sent via PasswordManager:onFormSubmit + is(origin, tc.origin, "Check origin"); + is(data.formActionOrigin, tc.formActionOrigin, "Check formActionOrigin"); + + if (tc.usernameFieldValue === null) { + is(data.usernameField, tc.usernameFieldValue, "Check usernameField"); + } else { + is(data.usernameField.value, tc.usernameFieldValue, "Check usernameField"); + } + + is(data.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue"); + + if (tc.oldPasswordFieldValue === null) { + is(data.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue"); + } else { + is(data.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue"); + } +}; + +let count = 0; +for (let tc of TESTCASES) { + for (let scriptName of Object.keys(SCRIPTS)) { + for (let surroundDocumentWithForm of [false, true]) { + let testDoc = tc.document; + if (surroundDocumentWithForm) { + if (testDoc.includes("<form")) { + info("Skipping surroundDocumentWithForm case since document already contains a <form>"); + continue; + } + testDoc = "<form>" + testDoc + "</form>"; + } + + if (["REMOVE_FORM", "REMOVE_FORM_PARENT"].includes(scriptName) && + (!testDoc.includes("<form") || tc.removePassword)) { + continue; + } else if (["REMOVE_PASSWORD","REMOVE_PASSWORD_PARENT"].includes(scriptName) && + testDoc.includes("<form")) { + continue; + } + + let taskName = `testcase-${count}-${scriptName}${surroundDocumentWithForm ? '-formWrapped' : ''}`; + let tmp = { + async [taskName]() { + info("Starting testcase with script " + scriptName + " and " + + (surroundDocumentWithForm ? "a" : "no") + " form wrapper: " + JSON.stringify(tc)); + await testFormlesSubmitFormRemoval(tc, testDoc, scriptName); + }, + }; + add_task(tmp[taskName]); + } + } + count++; +} + +</script> + +<p id="display"></p> + +<div id="content"> + <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal_negative.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal_negative.html new file mode 100644 index 0000000000..dfd7670a12 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_form_removal_negative.html @@ -0,0 +1,204 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test no capturing of fields outside of a form due to navigation</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> +SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive"); + +let loadPromise = new Promise(resolve => { + document.addEventListener("DOMContentLoaded", () => { + document.getElementById("loginFrame").addEventListener("load", (evt) => { + resolve(); + }); + }); +}); + +function submissionProcessed(...args) { + ok(false, "No formSubmissionProcessed should occur in this test"); + info("got: " + JSON.stringify(args)); +} + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["signon.formRemovalCapture.enabled", true], + ], + }); + + info("Waiting for page and frame loads"); + await loadPromise; + + PWMGR_COMMON_PARENT.addMessageListener("formSubmissionProcessed", submissionProcessed); + + SimpleTest.registerCleanupFunction(() => { + PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", submissionProcessed); + }); +}); + +const DEFAULT_ORIGIN = "http://test1.mochi.test:8888"; +const SCRIPTS = { + // Test form submission is not triggered when there is no user interaction and + // no ajax request fired previously. + REMOVE_TOP: `let e = document.querySelector("html"); e.parentNode.removeChild(e);`, + + // Test the following scripts don't trigger form submissions because of the + // form removal heuristics + PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`, + WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`, + WINDOW_LOCATION_RELOAD: `window.location.reload();`, + HISTORY_BACK: `history.back();`, + HISTORY_GO_MINUS1: `history.go(-1);`, +}; + +const HEURISTICS = [ + { + userInput: true, + ajaxSuccess: true, + }, + { + userInput: false, + ajaxSuccess: true, + }, + { + userInput: true, + ajaxSuccess: false, + }, + { + userInput: false, + ajaxSuccess: false, + }, +]; + +const TESTCASES = [ + // Begin test cases that shouldn't trigger capture. + { + // Empty password field in a form + document: `<form><input type=password value="xxx"></form>`, + selectorValues: { + "[type=password]": "", + }, + }, + { + // Empty password field + document: `<input type=password value="">`, + selectorValues: { + "[type=password]": "", + }, + }, + { + // Test with an input that would normally be captured but with SCRIPTS that + // shouldn't trigger capture. + document: `<input type=password value="">`, + selectorValues: { + "[type=password]": "pass2", + }, + wouldCapture: true, + }, + { + // Test with an input that would normally be captured but with SCRIPTS that + // shouldn't trigger capture. + document: `<form><input type=password value=""></form>`, + selectorValues: { + "[type=password]": "pass2", + }, + wouldCapture: true, + }, +]; + +async function testFormlesSubmitNavigationNegative(tc, scriptName, heuristic) { + let loginFrame = document.getElementById("loginFrame"); + let waitTime; + let android = navigator.appVersion.includes("Android"); + if (android) { + // intermittent failures on Android Debug at 5 seconds + waitTime = 10000; + } else { + waitTime = 5000; + } + + let loadedPromise = new Promise((resolve) => { + loginFrame.addEventListener("load", function() { + resolve(); + }, {once: true}); + }); + + loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"; + await loadedPromise; + + let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document; + let formsProcessed = promiseFormsProcessed(); + frameDoc.documentElement.innerHTML = tc.document; + await formsProcessed; + + // We eliminate no user input as a reason for not capturing by modifying the value + setUserInputValues(frameDoc.documentElement, tc.selectorValues, heuristic.userInput); + + if (heuristic.ajaxSuccess) { + await SpecialPowers.spawn(frameDoc.defaultView, [], async () => { + await content.fetch("http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"); + }); + } + + info("Running " + scriptName + " script to check for a submission"); + frameDoc.defaultView.eval(SCRIPTS[scriptName]); + + info("Running " + scriptName + " script to check for a submission 1"); + // Wait to see if the promise above resolves. + await new Promise(resolve => setTimeout(resolve, waitTime)); + info("Running " + scriptName + " script to check for a submission 2"); + ok(true, "Done waiting for captures"); + +} + +let count = 0; +for (let tc of TESTCASES) { + for (let scriptName of Object.keys(SCRIPTS)) { + for (let heuristic of HEURISTICS) { + let shouldCaptureAFormRemoval = heuristic.ajaxSuccess && heuristic.userInput; + // Only run the following scripts when we are going to observeform removal change + // to save some time running this whole test. + if (["PUSHSTATE", "WINDOW_LOCATION", "WINDOW_LOCATION_RELOAD", "HISTORY_BACK", "HISTORY_GO_MINUS1"].includes(scriptName)) { + if(!shouldCaptureAFormRemoval) { + continue; + } + + if (tc.wouldCapture && ["PUSHSTATE", "WINDOW_LOCATION"].includes(scriptName)) { + // Don't run scripts that should actually capture for this testcase. + continue; + } + } else if (["REMOVE_TOP"].includes(scriptName)) { + if (shouldCaptureAFormRemoval) { + // Don't run scripts that should actually capture for this testcase. + continue; + } + } + + let taskName = `testcase-${count}-${scriptName}`; + let tmp = { + async [taskName]() { + info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc) + ": " + JSON.stringify(heuristic)); + await testFormlesSubmitNavigationNegative(tc, scriptName, heuristic); + }, + }; + add_task(tmp[taskName]); + } + } +} + + +</script> + +<p id="display"></p> + +<div id="content"> + <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html new file mode 100644 index 0000000000..348669a85c --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html @@ -0,0 +1,267 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test capturing of fields outside of a form due to navigation</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> +let loadPromise = new Promise(resolve => { + document.addEventListener("DOMContentLoaded", () => { + document.getElementById("loginFrame").addEventListener("load", (evt) => { + resolve(); + }); + }); +}); + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["signon.formlessCapture.enabled", true], + ], + }); + + info("Waiting for page and frame loads"); + await loadPromise; + + await loadRecipes({ + siteRecipes: [{ + hosts: ["test1.mochi.test:8888"], + usernameSelector: "input[name='recipeuname']", + passwordSelector: "input[name='recipepword']", + }], + }); +}); + +const DEFAULT_ORIGIN = "http://test1.mochi.test:8888"; +const SCRIPTS = { + PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`, + WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`, +}; +const TESTCASES = [ + { + // Inputs + document: `<input type=password value="">`, + selectorValues: { + "[type=password]": "pass1", + }, + expectedFormsCount: 1, + + // Expected outputs similar to PasswordManager:onFormSubmit + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: null, + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + }, + { + document: `<input id="u1" value=""> + <input type=password value="">`, + selectorValues: { + "#u1": "user1", + "[type=password]": "pass1", + }, + + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + }, + { + document: `<input id="u1" value=""> + <input id="p1" type=password value=""> + <input id="p2" type=password value="">`, + selectorValues: { + "#u1": "user1", + "#p1": "pass1", + "#p2": "pass2", + }, + + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: "pass1", + }, + { + document: `<input id="u1" value=""> + <input id="p1" type=password value=""> + <input id="p2" type=password value=""> + <input id="p3" type=password value="">`, + selectorValues: { + "#u1": "user1", + "#p1": "pass1", + "#p2": "pass2", + "#p3": "pass2", + }, + + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: "pass1", + }, + { + // Since there are two FormLikes to auto-submit in this case we mark + // one FormLike's password fields with a magic "ignore-form-submission" + // value so we can just focus on the other form. We then repeat the testcase + // below with the other FormLike ignored. + document: `<input id="u1" value=""> + <input type=password id="p1" value="" form="form1"> + <input type=password id="p2" value=""> + <form id="form1"> + <input id="u2" value=""> + <input id="p3" type=password value=""> + </form>`, + selectorValues: { + "#u1": "user1", + "#p1": "ignore-form-submission", + "#p2": "pass1", + "#u2": "user3", + "#p3": "ignore-form-submission", + }, + expectedFormsCount: 2, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "user1", + newPasswordFieldValue: "pass1", + oldPasswordFieldValue: null, + }, + { // Same as above but with the other form ignored. + document: `<input id="u1" value=""> + <input id="p1" type=password value="" form="form1"> + <input id="p2" type=password value=""> + <form id="form1"> + <input id="u2" value=""> + <input id="p3" type=password value=""> + </form>`, + selectorValues: { + "#u1": "user1", + "#p1": "pass2", + "#p2": "ignore-form-submission", + "#u2": "user3", + "#p3": "pass2", + }, + + expectedFormsCount: 2, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: null, + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: null, + }, + { + document: `<!-- recipe field override --> + <input name="recipeuname" value=""> + <input id="u1" value=""> + <input id="p1" type=password value=""> + <input name="recipepword" type=password value="">`, + selectorValues: { + "[name='recipeuname']": "username from recipe", + "#u1": "default field username", + "#p1": "pass1", + "[name='recipepword']": "pass2", + }, + + expectedFormsCount: 1, + origin: DEFAULT_ORIGIN, + formActionOrigin: DEFAULT_ORIGIN, + usernameFieldValue: "username from recipe", + newPasswordFieldValue: "pass2", + oldPasswordFieldValue: null, + }, +]; + +function filterFormSubmissions({ origin, data }) { + return data.newPasswordField.value != "ignore-form-submission"; +} + +async function testFormlesSubmitNavigation(tc, testDoc, scriptName) { + + let loginFrame = document.getElementById("loginFrame"); + let loadedPromise = new Promise((resolve) => { + loginFrame.addEventListener("load", function() { + resolve(); + }, {once: true}); + }); + loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"; + await loadedPromise; + + let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document; + + let formsProcessed = promiseFormsProcessed(tc.expectedFormsCount); + frameDoc.documentElement.innerHTML = testDoc; + await formsProcessed; + // We eliminate no user input as a reason for not capturing by modifying the value + setUserInputValues(frameDoc.documentElement, tc.selectorValues) + + let submitProcessed = getSubmitMessage(filterFormSubmissions); + info("Running " + scriptName + " script to cause a submission"); + frameDoc.defaultView.eval(SCRIPTS[scriptName]); + + info("Waiting for formSubmissionProcsssed message"); + let { origin, data } = await submitProcessed; + info("Got for formSubmissionProcsssed message"); + + // Check data sent via PasswordManager:onFormSubmit + is(origin, tc.origin, "Check origin"); + is(data.formActionOrigin, tc.formActionOrigin, "Check formActionOrigin"); + + if (tc.usernameFieldValue === null) { + is(data.usernameField, tc.usernameFieldValue, "Check usernameField"); + } else { + is(data.usernameField.value, tc.usernameFieldValue, "Check usernameField"); + } + + is(data.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue"); + + if (tc.oldPasswordFieldValue === null) { + is(data.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue"); + } else { + is(data.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue"); + } +}; + +let count = 0; +for (let tc of TESTCASES) { + for (let scriptName of Object.keys(SCRIPTS)) { + for (let surroundDocumentWithForm of [false, true]) { + let testDoc = tc.document; + if (surroundDocumentWithForm) { + if (testDoc.includes("<form")) { + info("Skipping surroundDocumentWithForm case since document already contains a <form>"); + continue; + } + testDoc = "<form>" + testDoc + "</form>"; + } + let taskName = `testcase-${count}-${scriptName}${surroundDocumentWithForm ? '-formWrapped' : ''}`; + let tmp = { + async [taskName]() { + info("Starting testcase with script " + scriptName + " and " + + (surroundDocumentWithForm ? "a" : "no") + " form wrapper: " + JSON.stringify(tc)); + await testFormlesSubmitNavigation(tc, testDoc, scriptName); + }, + }; + add_task(tmp[taskName]); + } + } + count++; +} + +</script> + +<p id="display"></p> + +<div id="content"> + <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html new file mode 100644 index 0000000000..338cd5d2c1 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html @@ -0,0 +1,148 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test no capturing of fields outside of a form due to navigation</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> +SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive"); + +let loadPromise = new Promise(resolve => { + document.addEventListener("DOMContentLoaded", () => { + document.getElementById("loginFrame").addEventListener("load", (evt) => { + resolve(); + }); + }); +}); + +function submissionProcessed(...args) { + ok(false, "No formSubmissionProcessed should occur in this test"); + info("got: " + JSON.stringify(args)); +} + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["signon.formlessCapture.enabled", true], + ], + }); + + info("Waiting for page and frame loads"); + await loadPromise; + + PWMGR_COMMON_PARENT.addMessageListener("formSubmissionProcessed", submissionProcessed); + + SimpleTest.registerCleanupFunction(() => { + PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", submissionProcessed); + }); +}); + +const DEFAULT_ORIGIN = "http://test1.mochi.test:8888"; +const SCRIPTS = { + PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`, + WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`, + WINDOW_LOCATION_RELOAD: `window.location.reload();`, + HISTORY_BACK: `history.back();`, + HISTORY_GO_MINUS1: `history.go(-1);`, +}; +const TESTCASES = [ + // Begin test cases that shouldn't trigger capture. + { + // Empty password field in a form + document: `<form><input type=password value="xxx"></form>`, + selectorValues: { + "[type=password]": "", + }, + }, + { + // Empty password field + document: `<input type=password value="">`, + selectorValues: { + "[type=password]": "", + }, + }, + { + // Test with an input that would normally be captured but with SCRIPTS that + // shouldn't trigger capture. + document: `<input type=password value="">`, + selectorValues: { + "[type=password]": "pass2", + }, + wouldCapture: true, + }, + { + // Test with an input that would normally be captured but with SCRIPTS that + // shouldn't trigger capture. + document: `<form><input type=password value=""></form>`, + selectorValues: { + "[type=password]": "pass2", + }, + wouldCapture: true, + }, +]; + +async function testFormlesSubmitNavigationNegative(tc, scriptName) { + let loginFrame = document.getElementById("loginFrame"); + let waitTime; + let android = navigator.appVersion.includes("Android"); + if (android) { + // intermittent failures on Android Debug at 5 seconds + waitTime = 10000; + } else { + waitTime = 5000; + } + + let loadedPromise = new Promise((resolve) => { + loginFrame.addEventListener("load", function() { + resolve(); + }, {once: true}); + }); + loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"; + await loadedPromise; + + let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document; + frameDoc.documentElement.innerHTML = tc.document; + // We eliminate no user input as a reason for not capturing by modifying the value + setUserInputValues(frameDoc.documentElement, tc.selectorValues); + + info("Running " + scriptName + " script to check for a submission"); + frameDoc.defaultView.eval(SCRIPTS[scriptName]); + + // Wait to see if the promise above resolves. + await new Promise(resolve => setTimeout(resolve, waitTime)); + ok(true, "Done waiting for captures"); +} + +let count = 0; +for (let tc of TESTCASES) { + for (let scriptName of Object.keys(SCRIPTS)) { + if (tc.wouldCapture && ["PUSHSTATE", "WINDOW_LOCATION"].includes(scriptName)) { + // Don't run scripts that should actually capture for this testcase. + continue; + } + let taskName = `testcase-${count}-${scriptName}`; + let tmp = { + async [taskName]() { + info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc)); + await testFormlesSubmitNavigationNegative(tc, scriptName); + }, + }; + add_task(tmp[taskName]); + } +} + + +</script> + +<p id="display"></p> + +<div id="content"> + <iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_include_other_subdomains_in_lookup.html b/toolkit/components/passwordmgr/test/mochitest/test_include_other_subdomains_in_lookup.html new file mode 100644 index 0000000000..4c569463c5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_include_other_subdomains_in_lookup.html @@ -0,0 +1,202 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test includeOtherSubdomainsInLookup</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> + Test that logins with non-exact match origin appear in autocomplete dropdown. + + The includeOtherSubdomainsInLookup feature offers autocomplete results for + subdomain related logins. + + For this testcase, there exists two logins for this origin on different + subdomains (old.example.com and new.example.com) but with different + passwords, beneath one login for the parent domain (example.com). Both + logins should appear in the autocomplete popup. + +<template id="form1-template"> + <form id="form1" action="https://otherexample.com/formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + </form> +</template> + +<script class="testbody" type="text/javascript"> + const formTemplate = document.getElementById("form1-template"); + + const domainname = location.hostname; + const loginInfos = [ + [`https://${domainname}/`, `https://${domainname}/`, null, "example-user", "example-password", "uname", "pword"], + [`https://old.${domainname}/`, `https://old.${domainname}/`, null, "example-user", "example-password", "uname", "pword"], + [`https://new.${domainname}/`, `https://new.${domainname}/`, null, "example-user", "new-example-password", "uname", "pword"], + ]; + + // calculates a date representation as used in the autocomplete menu to + // distinguish duplicate logins + function getDateString() { + const dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, { dateStyle: "medium" }); + return dateAndTimeFormatter.format(new Date()); + } + + add_setup(async () => { + listenForUnexpectedPopupShown(); + }); + + add_named_task("not including https login when includeOtherSubdomainsInLookup is false", async () => { + await setPreferencesForTask(["signon.includeOtherSubdomainsInLookup", false]); + await setStoredLoginsDuringTask( + [`https://${domainname}/`, `https://${domainname}/`, null, "example-user-https", "example-password", "uname", "pword"], + ); + const form = setContentForTask(formTemplate); + form.uname.focus(); + + await noPopupByArrowDown(); + ok(true, "no popup has been opened") + }); + + add_named_task("including https login when includeOtherSubdomainsInLookup is true", async () => { + await setPreferencesForTask(["signon.includeOtherSubdomainsInLookup", true]); + await setStoredLoginsDuringTask( + [`https://${domainname}/`, `https://${domainname}/`, null, "example-user-https", "example-password", "uname", "pword"], + ); + const form = setContentForTask(formTemplate); + form.uname.focus(); + + const autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, ["example-user-https"], window.location.host, "https login showed"); + }); + + add_named_task("including http login when includeOtherSubdomainsInLookup is false", async () => { + await setPreferencesForTask(["signon.includeOtherSubdomainsInLookup", false]); + await setStoredLoginsDuringTask( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + [`http://${domainname}/`, `https://${domainname}/`, null, "example-user-http", "example-password", "uname", "pword"], + ); + const form = setContentForTask(formTemplate); + form.uname.focus(); + + const autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, ["example-user-http"], window.location.host, "http login showed"); + }); + + add_named_task("including http login when includeOtherSubdomainsInLookup is true", async () => { + await setPreferencesForTask(["signon.includeOtherSubdomainsInLookup", true]); + await setStoredLoginsDuringTask( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + [`http://${domainname}/`, `https://${domainname}/`, null, "example-user-http", "example-password", "uname", "pword"], + ); + const form = setContentForTask(formTemplate); + form.uname.focus(); + + const autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, ["example-user-http"], window.location.host, "http login showed"); + }); + + add_named_task("including https and http login when includeOtherSubdomainsInLookup is false", async () => { + await setPreferencesForTask(["signon.includeOtherSubdomainsInLookup", false]); + await setStoredLoginsDuringTask( + [`https://${domainname}/`, `https://${domainname}/`, null, "example-user-https", "example-password", "uname", "pword"], + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + [`http://${domainname}/`, `https://${domainname}/`, null, "example-user-http", "example-password", "uname", "pword"], + ); + const form = setContentForTask(formTemplate); + form.uname.focus(); + + const autocompleteItems = await popupByArrowDown(); + // checkAutoCompleteResults(autocompleteItems, ["example-user-https"], window.location.host, "https login showed"); + checkAutoCompleteResults(autocompleteItems, ["example-user-http"], window.location.host, "https login showed"); + }); + + add_named_task("including https and http login when includeOtherSubdomainsInLookup is true", async () => { + await setPreferencesForTask(["signon.includeOtherSubdomainsInLookup", true]); + await setStoredLoginsDuringTask( + [`https://${domainname}/`, `https://${domainname}/`, null, "example-user-https", "example-password", "uname", "pword"], + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + [`http://${domainname}/`, `https://${domainname}/`, null, "example-user-http", "example-password", "uname", "pword"], + ); + const form = setContentForTask(formTemplate); + form.uname.focus(); + + const autocompleteItems = await popupByArrowDown(); + // checkAutoCompleteResults(autocompleteItems, ["example-user-https"], window.location.host, "https login showed"); + checkAutoCompleteResults(autocompleteItems, ["example-user-http", "example-user-https"], window.location.host, "http and https logins showed"); + }); + + add_named_task("menu does not show login for different subdomain when includeOtherSubdomainsInLookup is false", async () => { + await setPreferencesForTask(["signon.includeOtherSubdomainsInLookup", false]); + await setStoredLoginsDuringTask( + ...[ + // These two logins should never be visible on https: versions of + // *.example.com since the login is for http: and an https: login exists + // for this username. + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + [`http://${domainname}/`, `https://${domainname}/`, null, "example-user", "example-password", "uname", "pword"], + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + [`http://old.${domainname}/`, `https://old.${domainname}/`, null, "example-user", "old-example-password", "uname", "pword"], + ...loginInfos, + ] + ); + const form = setContentForTask(formTemplate); + + form.uname.focus(); + const autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, ["example-user"], window.location.host, "Only exact subdomain showed"); + }); + + add_named_task("menu shows two logins with the same username for different subdomain", async () => { + await setStoredLoginsDuringTask(...loginInfos); + await setPreferencesForTask(["signon.includeOtherSubdomainsInLookup", true]); + const form = setContentForTask(formTemplate); + + // The logins are added "today" and since they are duplicates, the date that they were last + // changed will be appended. + const dateString = getDateString(); + const username = `example-user (${dateString})`; + + form.uname.focus(); + const autocompleteItems = await popupByArrowDown(); + checkAutoCompleteResults(autocompleteItems, [username, username], window.location.host, "Check all menuitems are displayed correctly."); + }); + + add_named_task("login with different subdomain is selectable", async () => { + await setPreferencesForTask(["signon.includeOtherSubdomainsInLookup", true]); + await setStoredLoginsDuringTask(...loginInfos); + const form = setContentForTask(formTemplate); + + // consume initial autofill event + await formAutofillResult(form.id); + + form.uname.focus(); + + // select first item + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + + await formAutofillResult(form.id); + is(form.uname.value, "example-user", "correct username is set"); + is(form.pword.value, "example-password", "password matches first login"); + + // select second item + await popupByArrowDown(); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + + await formAutofillResult(form.id); + is(form.uname.value, "example-user", "correct username is set"); + is(form.pword.value, "new-example-password", "password matches second login"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html new file mode 100644 index 0000000000..2560c212d8 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for input events in Login Manager</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: input events should fire. +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script> + add_setup(async () => { + await setStoredLoginsAsync( + [location.origin, location.origin, null, "testuser", "testpass", "uname", "pword"] + ); + }) + + add_task(async function username_events() { + return new Promise(resolve => { + let inputFired = false; + const form = createLoginForm(); + form.uname.oninput = e => { + is(e.target.value, "testuser", "Should get 'testuser' as value in input event"); + inputFired = true; + }; + form.uname.onchange = e => { + ok(inputFired, "Should get input event before change event for username field."); + is(e.target.value, "testuser", "Should get 'testuser' as value in change event"); + resolve(); + }; + }) + }) + + add_task(async function password_events() { + return new Promise(resolve => { + let inputFired = false; + const form = createLoginForm(); + form.pword.oninput = e => { + is(e.target.value, "testpass", "Should get 'testpass' as value in input event"); + inputFired = true; + }; + form.pword.onchange = e => { + ok(inputFired, "Should get input event before change event for password field."); + is(e.target.value, "testpass", "Should get 'testpass' as value in change event"); + resolve(); + }; + }) + }) +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html new file mode 100644 index 0000000000..c6e378a516 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_input_events_for_identical_values.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for input events in Login Manager when username/password are filled in already</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="onNewEvent(event)"> +Login Manager test: input events should fire. + +<script> +runChecksAfterCommonInit(); + +SimpleTest.requestFlakyTimeout("untriaged"); + +/** Test for Login Manager: form fill when form is already filled, should not get input events. **/ + +var onloadFired = false; + +function onNewEvent(e) { + console.error("Got " + e.type + " event."); + if (e.type == "load") { + onloadFired = true; + getFormElementByName(1, "uname").focus(); + sendKey("Tab"); + } else { + ok(false, "Got an input event for " + e.target.name + " field, which shouldn't happen."); + } +} +</script> + +<p id="display"></p> + +<div id="content"> + + <form id="form1" action="formtest.js"> + <p>This is form 1.</p> + <input type="text" name="uname" oninput="onNewEvent(event)" value="testuser"> + <input type="password" name="pword" oninput="onNewEvent(event)" onfocus="setTimeout(function() { SimpleTest.finish() }, 1000);" value="testpass"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html new file mode 100644 index 0000000000..8fcb9df6f6 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_no_saved_login.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test basic login, contextual inscure password warning without saved logins</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: contextual inscure password warning without saved logins + +<script> +let chromeScript = runChecksAfterCommonInit(); +</script> +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"> + + <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;"> + <input type="text" name="uname"> + <input type="password" name="pword"> + <button type="submit">Submit</button> + </form> + +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: contextual insecure password warning without saved logins. **/ + +let uname = getFormElementByName(1, "uname"); +let pword = getFormElementByName(1, "pword"); + +// Restore the form to the default state. +function restoreForm() { + uname.value = ""; + pword.value = ""; + uname.focus(); +} + +function spinEventLoop() { + return Promise.resolve(); +} + +add_setup(async () => { + listenForUnexpectedPopupShown(); +}); + +add_task(async function test_form1_initial_empty() { + await SimpleTest.promiseFocus(window); + + // Make sure initial form is empty. + checkLoginForm(uname, "", pword, ""); + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is initially closed"); +}); + +add_task(async function test_form1_warning_entry() { + await SimpleTest.promiseFocus(window); + // Trigger autocomplete popup + restoreForm(); + await popupBy(); + + let popupState = await getPopupState(); + is(popupState.open, true, "Check popup is opened"); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + + let acEvents = await getTelemetryEvents({ process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }); + is(acEvents.length, 1, "One autocomplete event"); + checkACTelemetryEvent(acEvents[0], uname, { + "hadPrevious": "0", + "insecureWarning": "1", + "loginsFooter": "1" + }); + + synthesizeKey("KEY_ArrowDown"); // select insecure warning + checkLoginForm(uname, "", pword, ""); // value shouldn't update just by selecting + synthesizeKey("KEY_Enter"); + await spinEventLoop(); // let focus happen + checkLoginForm(uname, "", pword, ""); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html new file mode 100644 index 0000000000..a61812f6d3 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_maxlength.html @@ -0,0 +1,144 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for maxlength attributes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: Bug 391514 +<script> +gTestDependsOnDeprecatedLogin = true; +runChecksAfterCommonInit(() => startTest()); + +let DEFAULT_ORIGIN = window.location.origin; +</script> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/* Test for Login Manager: 391514 (Login Manager gets confused with + * password/PIN on usaa.com) + */ + +async function startTest() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <!-- normal form. --> + <form id="form1" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- limited username --> + <form id="form2" action="formtest.js"> + <input type="text" name="uname" maxlength="4"> + <input type="password" name="pword"> + </form> + + <!-- limited password --> + <form id="form3" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword" maxlength="4"> + </form> + + <!-- limited username and password --> + <form id="form4" action="formtest.js"> + <input type="text" name="uname" maxlength="4"> + <input type="password" name="pword" maxlength="4"> + </form> + + + <!-- limited username --> + <form id="form5" action="formtest.js"> + <input type="text" name="uname" maxlength="0"> + <input type="password" name="pword"> + </form> + + <!-- limited password --> + <form id="form6" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword" maxlength="0"> + </form> + + <!-- limited username and password --> + <form id="form7" action="formtest.js"> + <input type="text" name="uname" maxlength="0"> + <input type="password" name="pword" maxlength="0"> + </form> + + + <!-- limited, but ok, username --> + <form id="form8" action="formtest.js"> + <input type="text" name="uname" maxlength="999"> + <input type="password" name="pword"> + </form> + + <!-- limited, but ok, password --> + <form id="form9" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword" maxlength="999"> + </form> + + <!-- limited, but ok, username and password --> + <form id="form10" action="formtest.js"> + <input type="text" name="uname" maxlength="999"> + <input type="password" name="pword" maxlength="999"> + </form> + + + <!-- limited, but ok, username --> + <!-- (note that filled values are exactly 8 characters) --> + <form id="form11" action="formtest.js"> + <input type="text" name="uname" maxlength="8"> + <input type="password" name="pword"> + </form> + + <!-- limited, but ok, password --> + <!-- (note that filled values are exactly 8 characters) --> + <form id="form12" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword" maxlength="8"> + </form> + + <!-- limited, but ok, username and password --> + <!-- (note that filled values are exactly 8 characters) --> + <form id="form13" action="formtest.js"> + <input type="text" name="uname" maxlength="8"> + <input type="password" name="pword" maxlength="8"> + </form>`, win, 13); + + var i; + + await checkLoginFormInFrameWithElementValues(win, 1, "testuser", "testpass"); + + for (i = 2; i < 8; i++) { + await checkLoginFormInFrameWithElementValues(win, i, "", ""); + } + + for (i = 8; i < 14; i++) { + await checkLoginFormInFrameWithElementValues(win, i, "testuser", "testpass"); + } + + // Note that tests 11-13 are limited to exactly the expected value. + // Assert this lest someone change the login we're testing with. + await SpecialPowers.spawn(win, [11, 8], (formNum, length) => { + let form = this.content.document.getElementById(`form${formNum}`); + let field = form.querySelector("[name='uname']"); + is(field.value.length, length, "asserting test assumption is valid."); + }); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_munged_values.html b/toolkit/components/passwordmgr/test/mochitest/test_munged_values.html new file mode 100644 index 0000000000..d2fdf91d4a --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_munged_values.html @@ -0,0 +1,362 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test handling of possibly-manipulated username values</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> +const DEFAULT_ORIGIN = window.location.origin; + +function removeAllUserFacingLoginsInParent() { + runInParent(function removeAllUserFacingLogins() { + Services.logins.removeAllUserFacingLogins(); + }); +} + +async function add2logins() { + removeAllUserFacingLoginsInParent(); + await addLoginsInParent([DEFAULT_ORIGIN, DEFAULT_ORIGIN, null, "real••••user", "pass1", "", ""], [DEFAULT_ORIGIN, DEFAULT_ORIGIN, null, "user2", "pass2", "", ""]); +} + +async function addSingleLogin() { + removeAllUserFacingLoginsInParent(); + await addLoginsInParent([DEFAULT_ORIGIN, DEFAULT_ORIGIN, null, "real••••user", "pass1", "", ""]) +} + +/** + * For any test including the character "!", generate a version of that test for every known munge + * character. + **/ + function generateTestCases(test) { + const MUNGE_CHARS = ["*", ".", "•"]; + + const nothingToReplace = Object.values(test).every(value => typeof value !== "string" || !value.includes("!")); + if (nothingToReplace) { + return test; + }; + + return MUNGE_CHARS.map(char => { + const newTest = {}; + for (const [propName, val] of Object.entries(test)) { + if (typeof val === "string") { + newTest[propName] = val.replace(/!/g, char); + } else { + newTest[propName] = val; + } + }; + return newTest; +})}; + +const loadPromise = new Promise(resolve => { + document.addEventListener("DOMContentLoaded", () => { + resolve(); + }); +}); + +add_setup(async () => { + await setStoredLoginsAsync(); + info("Waiting for page and window loads"); + await loadPromise; +}); + +add_task(async function test_new_logins() { + const TEST_CASES = [ + // ! is replaced with characters commonly used for munging + { + testName: "test_middle!MaskedUsername", + username: "so!!!ne", + expected: null, + }, + { + testName: "test_start!MaskedUsername", + username: "!!!eone", + expected: null, + }, + { + testName: "test_end!MaskedUsername", + username: "some!!!", + expected: null, + }, + { + testName: "test_ok!Username", + username: "obelixand!", + expected: "obelixand!", + }, + { + testName: "test_ok!Username2", + username: "!!username!!", + expected: "!!username!!", + }, + { + // We should only consider a username munged if it repeats of the same character + testName: "test_combinedMungeCharacters", + username: "*.•*.•*.•*.•*.•*.•", + expected: "*.•*.•*.•*.•*.•*.•", + }, +].flatMap(generateTestCases); + for (const tc of TEST_CASES) { + info("Starting testcase: " + JSON.stringify(tc)); + // Create a new window for each test case, because if we instead try to use + // the same window and change the page using window.location, that will trigger + // an onLocationChange event, which can trigger unwanted FormSubmit outside of + // clicking the submit button in each test document. + const win = window.open("about:blank"); + const html = ` + <form id="form1" onsubmit="return false;"> + <input type="text" name="uname" value="${tc.username}"> + <input type="password" name="pword" value="thepassword"> + <button type="submit" id="submitBtn">Submit</button> + </form>`; + await loadFormIntoWindow(DEFAULT_ORIGIN, html, win); + await SpecialPowers.spawn(win, [html], function(contentHtml) { + const doc = this.content.document; + for (const field of doc.querySelectorAll("input")) { + const actualValue = field.value; + field.value = ""; + SpecialPowers.wrap(field).setUserInput(actualValue); + } + }); + await SpecialPowers.spawn(win, [tc], function(testcase) { + const doc = this.content.document; + Assert.equal(doc.querySelector("[name='uname']").value, testcase.username, "Checking for filled username"); + }); + + // Check data sent via PasswordManager:onFormSubmit + const processedPromise = getSubmitMessage(); + await SpecialPowers.spawn(win, [], function() { + this.content.document.getElementById("submitBtn").click(); + }); + + const { data } = await processedPromise; + info("Got submitted result: " + JSON.stringify(data)); + + if (tc.expected === null) { + is(data.usernameField, tc.expected, "Check usernameField"); + } else { + is(data.usernameField.value, tc.expected, "Check usernameField"); + } + + win.close(); + await SimpleTest.promiseFocus(window); + } +}); + +add_task(async function test_no_save_dialog_when_password_is_fully_munged() { + const TEST_CASES = [ + { + testName: "test_passFullyMungedBy!", + password: "!!!!!!!!!", + shouldShowPrompt: false, + }, + { + testName: "test_passStartsMungedBy!", + password: "!!!!!!!butThenAPassword", + shouldShowPrompt: true, + }, + { + testName: "test_passEndsMungedBy!", + password: "aRealPasswordAndThen!!!!!!!", + shouldShowPrompt: true, + }, + { + testName: "test_passMostlyMungedBy!", + password: "!!!a!!!!", + shouldShowPrompt: true, + }, + { + testName: "test_combinedMungedCharacters", + password: "*.•*.•*.•*.•", + shouldShowPrompt: true, + }, + ].flatMap(generateTestCases); + + for (const tc of TEST_CASES) { + info("Starting testcase: " + tc.testName) + // Create a new window for each test case, because if we instead try to use + // the same window and change the page using window.location, that will trigger + // an onLocationChange event, which can trigger unwanted FormSubmit outside of + // clicking the submit button in each test document. + const win = window.open("about:blank"); + const html = ` + <form id="form1" onsubmit="return false;"> + <input type="text" name="uname" value="username"> + <input type="password" name="pword" value="${tc.password}"> + <button type="submit" id="submitBtn">Submit</button> + </form>`; + await loadFormIntoWindow(DEFAULT_ORIGIN, html, win); + await SpecialPowers.spawn(win, [html], function(contentHtml) { + const doc = this.content.document; + for (const field of doc.querySelectorAll("input")) { + const actualValue = field.value; + field.value = ""; + SpecialPowers.wrap(field).setUserInput(actualValue); + } + }); + await SpecialPowers.spawn(win, [tc], function(testcase) { + const doc = this.content.document; + Assert.equal(doc.querySelector("[name='pword']").value, testcase.password, "Checking for filled password"); + }); + + const formSubmitListener = SpecialPowers.spawn(win, [], function() { + return new Promise(resolve => { + this.content.windowRoot.addEventListener( + "PasswordManager:ShowDoorhanger", + event => { + info(`PasswordManager:ShowDoorhanger called. Event: ${JSON.stringify(event)}`); + resolve(event.detail.messageSent); + } + ); + }); + }); + + await SpecialPowers.spawn(win, [], function() { + this.content.document.getElementById("submitBtn").click(); + }); + + const dialogRequested = await formSubmitListener; + + is(dialogRequested, tc.shouldShowPrompt, "Verify 'show save/update prompt' message sent to parent process"); + + win.close(); + await SimpleTest.promiseFocus(window); + } +}); + +add_task(async function test_no_autofill_munged_username_matching_password() { + // run this test with 2 matching logins from this origin so we don't autofill + await add2logins(); + const allLogins = await LoginManager.getAllLogins(); + const matchingLogins = Array.prototype.filter.call(allLogins, l => l.origin == DEFAULT_ORIGIN); + is(matchingLogins.length, 2, "Expected number of matching logins"); + + const bulletLogin = matchingLogins.find(l => l.username == "real••••user"); + ok(bulletLogin, "Found the real••••user login"); + + const timesUsed = bulletLogin.timesUsed; + const guid = bulletLogin.guid; + + const win = window.open("about:blank"); + const html = + `<form id="form1" onsubmit="return false;"> + <input type="text" name="uname" value=""> + <input type="password" name="pword" value=""> + <button type="submit" id="submitBtn">Submit</button> + </form>`; + await loadFormIntoWindow(DEFAULT_ORIGIN, html, win); + await SpecialPowers.spawn(win, [html], function(contentHtml) { + const doc = this.content.document; + for (const field of doc.querySelectorAll("input")) { + const actualValue = field.value; + field.value = ""; + SpecialPowers.wrap(field).setUserInput(actualValue); + } + }); + await SpecialPowers.spawn(win, [], function() { + const doc = this.content.document; + Assert.equal(doc.querySelector("[name='uname']").value, "", "Check username didn't get autofilled"); + SpecialPowers.wrap(doc.querySelector("[name='uname']")).setUserInput("real••••user"); + SpecialPowers.wrap(doc.querySelector("[name='pword']")).setUserInput("pass1"); + }); + + // we shouldn't get the save password doorhanger... + const popupShownPromise = noPopupBy(); + + // Check data sent via PasswordManager:onFormSubmit + const processedPromise = getSubmitMessage(); + await SpecialPowers.spawn(win, [], function() { + this.content.document.getElementById("submitBtn").click(); + }); + + const { data } = await processedPromise; + info("Got submitted result: " + JSON.stringify(data)); + + is(data.usernameField, null, "Check usernameField"); + + const updatedLogins = await LoginManager.getAllLogins(); + const updatedLogin = Array.prototype.find.call(updatedLogins, l => l.guid == guid); + ok(updatedLogin, "Got the login via guid"); + is(updatedLogin.timesUsed, timesUsed + 1, "timesUsed was incremented"); + + await popupShownPromise; + + win.close(); + await SimpleTest.promiseFocus(window); +}); + + +add_task(async function test_autofill_munged_username_matching_password() { + // only a single matching login so we autofill the username + await addSingleLogin(); + + const allLogins = await LoginManager.getAllLogins(); + const matchingLogins = Array.prototype.filter.call(allLogins, l => l.origin == DEFAULT_ORIGIN); + is(matchingLogins.length, 1, "Expected number of matching logins"); + + info("matched login: " + matchingLogins[0].username); + const bulletLogin = matchingLogins.find(l => l.username == "real••••user"); + ok(bulletLogin, "Found the real••••user login"); + + const timesUsed = bulletLogin.timesUsed; + const guid = bulletLogin.guid; + + const win = window.open("about:blank"); + const html = + `<form id="form1" onsubmit="return false;"> + <input type="text" name="uname" value=""> + <input type="password" name="pword" value=""> + <button type="submit" id="submitBtn">Submit</button> + </form>`; + await loadFormIntoWindow(DEFAULT_ORIGIN, html, win); + await SpecialPowers.spawn(win, [html], function(contentHtml) { + const doc = this.content.document; + for (const field of doc.querySelectorAll("input")) { + const actualValue = field.value; + field.value = ""; + SpecialPowers.wrap(field).setUserInput(actualValue); + } + }); + await SpecialPowers.spawn(win, [], function() { + const doc = this.content.document; + Assert.equal(doc.querySelector("[name='uname']").value, "real••••user", "Check username did get autofilled"); + doc.querySelector("[name='pword']").setUserInput("pass1"); + }); + + // we shouldn't get the save/update password doorhanger as it didn't change + const popupShownPromise = noPopupBy(); + + // Check data sent via PasswordManager:onFormSubmit + const processedPromise = getSubmitMessage(); + await SpecialPowers.spawn(win, [], function() { + this.content.document.getElementById("submitBtn").click(); + }); + + const { data } = await processedPromise; + info("Got submitted result: " + JSON.stringify(data)); + + is(data.usernameField, null, "Check usernameField"); + + const updatedLogins = await LoginManager.getAllLogins(); + const updatedLogin = Array.prototype.find.call(updatedLogins, l => l.guid == guid); + ok(updatedLogin, "Got the login via guid"); + is(updatedLogin.timesUsed, timesUsed + 1, "timesUsed was incremented"); + + await popupShownPromise; + + win.close(); + await SimpleTest.promiseFocus(window); +}); + +</script> + +<p id="display"></p> + +<div id="content"> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_one_doorhanger_per_un_pw.html b/toolkit/components/passwordmgr/test/mochitest/test_one_doorhanger_per_un_pw.html new file mode 100644 index 0000000000..4d8dfd1fee --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_one_doorhanger_per_un_pw.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Don't repeatedly prompt to save the same username and password + combination in the same document</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> + let chromeScript = runChecksAfterCommonInit(); + + SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popupshown to occur"); +</script> +<p id="display"></p> + +<div id="content" style="display: none"> + <form id="form1" onsubmit="return false;"> + <input type="text" name="uname" id="ufield"> + <input type="password" name="pword" id="pfield"> + <button type="submit" id="submitBtn">Submit</button> + </form> +</div> + +<pre id="test"></pre> +<script> + /** Test for Login Manager: Don't repeatedly prompt to save the + same username and password combination in the same document **/ + + add_task(async function test_prompt_does_not_reappear() { + let username = document.getElementById("ufield"); + let password = document.getElementById("pfield"); + let submitButton = document.getElementById("submitBtn"); + + SpecialPowers.wrap(username).setUserInput("user"); + SpecialPowers.wrap(password).setUserInput("pass"); + + let processedPromise = getSubmitMessage(); + let promptShownPromise = promisePromptShown("passwordmgr-prompt-save"); + submitButton.click(); + await processedPromise; + await promptShownPromise; + + is(username.value, "user", "Checking for filled username"); + is(password.value, "pass", "Checking for filled password"); + + let promptShown = false; + promptShownPromise = promisePromptShown("passwordmgr-prompt-save").then(value => { + promptShown = true; + }); + submitButton.click(); + await new Promise(resolve => setTimeout(resolve, 1000)); + ok(!promptShown, "Prompt is not shown for the same login values a second time"); + }); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_onsubmit_value_change.html b/toolkit/components/passwordmgr/test/mochitest/test_onsubmit_value_change.html new file mode 100644 index 0000000000..18b851c86f --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_onsubmit_value_change.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test input value change right after onsubmit event</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: input value change right after onsubmit event + +<script> + let chromeScriptPromise = runChecksAfterCommonInit(); + + async function getSubmitMessage() { + info("getSubmitMessage"); + let chromeScript = await chromeScriptPromise; + return new Promise(resolve => { + chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) { + info("got formSubmissionProcessed"); + chromeScript.removeMessageListener("formSubmissionProcessed", processed); + resolve(args[0]); + }); + }); + } +</script> +<p id="display"></p> + +<div id="content" style="display: none"> + + <form id="form1" action="formTest.js" onsubmit="return false;"> + <input type="text" name="uname" id="ufield"> + <input type="password" name="pword" id="pfield"> + <button type="submit" id="submitBtn">Submit</button> + </form> + +</div> + +<pre id="test"></pre> +<script> + /** Test for Login Manager: input value change right after onsubmit event **/ + add_task(async function checkFormValues() { + SpecialPowers.wrap(document.getElementById("ufield")).setUserInput("testuser"); + SpecialPowers.wrap(document.getElementById("pfield")).setUserInput("testpass"); + is(getFormElementByName(1, "uname").value, "testuser", "Checking for filled username"); + is(getFormElementByName(1, "pword").value, "testpass", "Checking for filled password"); + + document.getElementById("form1").addEventListener("submit", () => { + // deliberately assign to .value rather than setUserInput: + // the scenario under test here is that script is changing/populating + // fields after the user has clicked the submit button + document.getElementById("ufield").value = "newuser"; + document.getElementById("pfield").value = "newpass"; + }, true); + + document.getElementById("form1").addEventListener("submit", (e) => e.preventDefault()); + + let processedPromise = getSubmitMessage(); + + let button = document.getElementById("submitBtn"); + button.click(); + + let { data } = await processedPromise; + is(data.usernameField.value, "testuser", "Should have registered \"testuser\" for username"); + is(data.newPasswordField.value, "testpass", "Should have registered \"testpass\" for username"); + }); +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html new file mode 100644 index 0000000000..cb7f759cab --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_password_field_autocomplete.html @@ -0,0 +1,185 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test basic login autocomplete</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: multiple login autocomplete +<p id="display"></p> + +<!-- we presumably can't hide the content for this test. --> +<div id="content"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +/** Test for Login Manager: multiple login autocomplete. **/ + +const INSECURE_WARNING_TEXT = "This connection is not secure. Logins entered here could be compromised. Learn More"; + +// Restore the form to the default state. +async function reinitializeForm(index) { + // Using innerHTML is for creating the autocomplete popup again, so the + // preference value will be applied to the constructor of + // LoginAutoCompleteResult. + let form = document.getElementById("form" + index); + let temp = form.innerHTML; + form.innerHTML = ""; + form.innerHTML = temp; + + await new Promise(resolve => { + let observer = SpecialPowers.wrapCallback(() => { + SpecialPowers.removeObserver(observer, "passwordmgr-processed-form"); + resolve(); + }); + SpecialPowers.addObserver(observer, "passwordmgr-processed-form"); + }); + + await SimpleTest.promiseFocus(window); + + const uname = getFormElementByName(index, "uname"); + const pword = getFormElementByName(index, "pword"); + uname.value = ""; + pword.value = ""; + pword.focus(); +} + +function generateDateString(date) { + let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, + { dateStyle: "medium" }); + return dateAndTimeFormatter.format(date); +} + +const DATE_NOW_STRING = generateDateString(new Date()); + +// Check for expected username/password in form. +function checkACFormPasswordField(expectedPassword) { + const pword = getFormElementByName(1, "pword"); + is(pword.value, expectedPassword, "Checking form1 password is: " + JSON.stringify(expectedPassword)); +} + +async function userOpenAutocompleteOnForm1(autoFillInsecureForms) { + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.autofillForms.http", autoFillInsecureForms], + ]}); + await reinitializeForm(1); + const autocompleteItems = await popupBy(); + + const popupState = await getPopupState(); + is(popupState.selectedIndex, -1, "Check no entries are selected upon opening"); + + const expectedMenuItems = [INSECURE_WARNING_TEXT, + "No username (" + DATE_NOW_STRING + ")", + "tempuser1", + "testuser2", + "testuser3", + "zzzuser4"]; + checkAutoCompleteResults(autocompleteItems, expectedMenuItems, "mochi.test", "Check all menuitems are displayed correctly."); +} + +async function userPressedDown_passwordIs(value) { + synthesizeKey("KEY_ArrowDown"); + await Promise.resolve(); // let focus happen + checkACFormPasswordField(value); +} + +async function userPressedEnter_passwordIs(value) { + synthesizeKey("KEY_Enter"); + await Promise.resolve(); // let focus happen + checkACFormPasswordField(value); +} + +async function noPopupOnForm(formIndex, reason) { + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.autofillForms.http", true], + ]}); + await reinitializeForm(formIndex); + + // Trigger autocomplete popup + synthesizeKey("KEY_ArrowDown"); // open + let popupState = await getPopupState(); + is(popupState.open, false, reason); +} + +add_setup(async () => { + await setStoredLoginsAsync( + // login0 has no username, so should be filtered out from the autocomplete list. + [location.origin, "http://autocomplete:8888", null, "", "user0pass", "", "pword"], + [location.origin, "http://autocomplete:8888", null, "tempuser1", "temppass1", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "testuser2", "testpass2", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "testuser3", "testpass3", "uname", "pword"], + [location.origin, "http://autocomplete:8888", null, "zzzuser4", "zzzpass4", "uname", "pword"] + ); + createLoginForm({ + num: 1, + action: "http://autocomplete:8888/formtest.js" + }); + createLoginForm({ + num: 2, + actio: "http://autocomplete:8888/formtest.js", + password: { + readonly: true + } + }); + createLoginForm({ + num: 3, + action: "http://autocomplete:8888/formtest.js", + password: { + disabled: true + } + }); + listenForUnexpectedPopupShown(); +}); + +add_task(async function form1_initial_empty() { + await SimpleTest.promiseFocus(window); + + // Make sure initial form is empty. + checkACFormPasswordField(""); + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is initially closed"); +}); + +add_task(async function noAutocompleteForReadonlyField() { + await noPopupOnForm(2, "Check popup is closed for a readonly field."); +}); + +add_task(async function noAutocompleteForDisabledField() { + await noPopupOnForm(3, "Check popup is closed for a disabled field."); +}); + +add_task(async function insecureAutoFill_EnterOnWarning() { + await userOpenAutocompleteOnForm1(true); + await userPressedDown_passwordIs(""); + await userPressedEnter_passwordIs(""); +}); + +add_task(async function insecureAutoFill_EnterOnLogin() { + await userOpenAutocompleteOnForm1(true); + await userPressedDown_passwordIs(""); // select insecure warning + await userPressedDown_passwordIs(""); // select login + await userPressedEnter_passwordIs("user0pass"); +}); + +add_task(async function noInsecureAutoFill_EnterOnWarning() { + await userOpenAutocompleteOnForm1(false); + await userPressedDown_passwordIs(""); // select insecure warning + await userPressedEnter_passwordIs(""); +}); + +add_task(async function noInsecureAutoFill_EnterOnLogin() { + await userOpenAutocompleteOnForm1(false); + await userPressedDown_passwordIs(""); // select insecure warning + await userPressedDown_passwordIs(""); // select login + await userPressedEnter_passwordIs("user0pass"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_password_length.html b/toolkit/components/passwordmgr/test/mochitest/test_password_length.html new file mode 100644 index 0000000000..724caf236f --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_password_length.html @@ -0,0 +1,145 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test handling of different password length</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="application/javascript"> +let readyPromise = registerRunTests(); + +let loadPromise = new Promise(resolve => { + document.addEventListener("DOMContentLoaded", () => { + document.getElementById("loginFrame").addEventListener("load", (evt) => { + resolve(); + }); + }); +}); + +async function loadFormIntoIframe(origin, html) { + let loginFrame = document.getElementById("loginFrame"); + let loadedPromise = new Promise((resolve) => { + loginFrame.addEventListener("load", function() { + resolve(); + }, {once: true}); + }); + let processedPromise = promiseFormsProcessed(); + loginFrame.src = origin + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"; + await loadedPromise; + + await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [html], function(contentHtml) { + this.content.document.documentElement.innerHTML = contentHtml; + }); + + // Wait for the form to be processed before trying to submit. + await processedPromise; +} + +add_setup(async () => { + info("Waiting for setup and page and frame loads"); + await readyPromise; + await loadPromise; +}); + +const DEFAULT_ORIGIN = window.location.origin; +const TESTCASES = [ + { + testName: "test_control2PasswordFields", + pword1: "pass1", + pword2: "pass2", + expectedNewPassword: { value: "pass2" }, + expectedOldPassword: { value: "pass1" }, + }, + { + testName: "test_1characterPassword", + pword1: "x", + pword2: "pass2", + expectedNewPassword: { value: "pass2" }, + expectedOldPassword: null, + }, + { + testName: "test_2characterPassword", + pword1: "xy", + pword2: "pass2", + expectedNewPassword: { value: "pass2" }, + expectedOldPassword: { value: "xy" }, + }, + { + testName: "test_1characterNewPassword", + pword1: "pass1", + pword2: "x", + expectedNewPassword: { value: "pass1" }, + expectedOldPassword: null, + }, +]; + +/** + * @return {Promise} resolving when form submission was processed. + */ +function getSubmitMessage() { + return new Promise((resolve, reject) => { + PWMGR_COMMON_PARENT.addMessageListener("formSubmissionProcessed", function processed(...args) { + info("got formSubmissionProcessed"); + PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", processed); + resolve(args[0]); + }); + }); +} + +add_task(async function test_password_lengths() { + for (let tc of TESTCASES) { + info("Starting testcase: " + tc.testName + ", " + JSON.stringify([tc.pword1, tc.pword2])); + await loadFormIntoIframe(DEFAULT_ORIGIN, `<form id="form1" onsubmit="return false;"> + <input type="text" name="uname" value="myname"> + <input type="password" name="pword1" value=""> + <input type="password" name="pword2" value=""> + <button type="submit" id="submitBtn">Submit</button> + </form>`); + + await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [tc], function(testcase) { + let doc = this.content.document; + Assert.equal(doc.querySelector("[name='uname']").value, "myname", "Checking for filled username"); + doc.querySelector("[name='pword1']").setUserInput(testcase.pword1); + doc.querySelector("[name='pword2']").setUserInput(testcase.pword2); + }); + + // Check data sent via PasswordManager:onFormSubmit + let processedPromise = getSubmitMessage(); + await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() { + this.content.document.getElementById("submitBtn").click(); + }); + + let { data } = await processedPromise; + info("Got submitted result: " + JSON.stringify(data)); + + if (tc.expectedNewPassword === null) { + is(data.newPasswordField, + tc.expectedNewPassword, "Check expectedNewPassword is null"); + } else { + is(data.newPasswordField.value, + tc.expectedNewPassword.value, + "Check that newPasswordField.value matches the expectedNewPassword.value"); + } + if (tc.expectedOldPassword === null) { + is(data.oldPasswordField, + tc.expectedOldPassword, "Check expectedOldPassword is null"); + } else { + is(data.oldPasswordField.value, + tc.expectedOldPassword.value, + "Check that oldPasswordField.value matches expectedOldPassword.value"); + } + } +}); +</script> + +<p id="display"></p> + +<div id="content"> + <iframe id="loginFrame" src="/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html new file mode 100644 index 0000000000..fcaeb0e455 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_passwords_in_type_password.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test that passwords only get filled in type=password</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: Bug 242956 +<script> +gTestDependsOnDeprecatedLogin = true; +runChecksAfterCommonInit(() => startTest()); + +let DEFAULT_ORIGIN = window.location.origin; +</script> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: 242956 (Stored password is inserted into a + readable text input on a second page) **/ + +// Make sure that pwmgr only puts passwords into type=password <input>s. +// Might as well test the converse, too (username in password field). + +async function startTest() { + let win = window.open("about:blank"); + SimpleTest.registerCleanupFunction(() => win.close()); + + // only 4 out of 7 forms are to be autofilled + await loadFormIntoWindow(DEFAULT_ORIGIN, ` + <!-- pword is not a type=password input --> + <form id="form1" action="formtest.js"> + <input type="text" name="uname"> + <input type="text" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- uname is not a type=text input --> + <form id="form2" action="formtest.js"> + <input type="password" name="uname"> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- two "pword" inputs, (text + password) --> + <form id="form3" action="formtest.js"> + <input type="text" name="uname"> + <input type="text" name="pword"> + <input type="password" name="qword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- same thing, different order --> + <form id="form4" action="formtest.js"> + <input type="text" name="uname"> + <input type="password" name="pword"> + <input type="text" name="qword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- uname is not a type=text input (try a checkbox just for variety) --> + <form id="form5" action="formtest.js"> + <input type="checkbox" name="uname" value=""> + <input type="password" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- pword is not a type=password input (try a checkbox just for variety) --> + <form id="form6" action="formtest.js"> + <input type="text" name="uname"> + <input type="checkbox" name="pword" value=""> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form> + + <!-- pword is not a type=password input --> + <form id="form7" action="formtest.js"> + <input type="text" name="uname" value="testuser"> + <input type="text" name="pword"> + + <button type="submit">Submit</button> + <button type="reset"> Reset </button> + </form>`, win, 4); + + await checkLoginFormInFrameWithElementValues(win, 1, "", ""); + await checkLoginFormInFrameWithElementValues(win, 2, "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, 3, "", "testuser", "testpass"); + await checkLoginFormInFrameWithElementValues(win, 4, "testuser", "testpass", ""); + await checkLoginFormInFrameWithElementValues(win, 5, "", "testpass"); + await checkLoginFormInFrameWithElementValues(win, 6, "", ""); + await checkLoginFormInFrameWithElementValues(win, 7, "testuser", ""); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_primary_password.html b/toolkit/components/passwordmgr/test/mochitest/test_primary_password.html new file mode 100644 index 0000000000..4da6929b61 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_primary_password.html @@ -0,0 +1,296 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for primary password</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: primary password. + +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +"use strict"; + +// Force parent to not look for tab-modal prompts, as they're not used for auth prompts. +modalType = Ci.nsIPrompt.MODAL_TYPE_WINDOW; + +const exampleCom = "https://example.com/tests/toolkit/components/passwordmgr/test/mochitest/"; +const exampleOrg = "https://example.org/tests/toolkit/components/passwordmgr/test/mochitest/"; + +const win = window.open("about:blank"); +SimpleTest.registerCleanupFunction(() => win.close()); + +add_setup(async () => { + await setStoredLoginsAsync( + ["http://mochi.test:8888", "http://mochi.test:8888", null, "testuser", "testpass", "uname", "pword"], + ["https://example.com", "https://example.com", null, "user1", "pass1", "uname", "pword"], + ["https://example.org", "https://example.org", null, "user2", "pass2", "uname", "pword"] + ); + ok(await isLoggedIn(), "should be initially logged in (no PP)"); + enablePrimaryPassword(); + ok(!await isLoggedIn(), "should be logged out after setting PP"); +}); + +add_task(async function test_1() { + // Trigger a MP prompt via the API + const state = { + msg: "Please enter your Primary Password.", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + const action = { + buttonClick: "ok", + passField: LoginTestUtils.primaryPassword.primaryPassword, + }; + const promptDone = handlePrompt(state, action); + + const logins = await LoginManager.getAllLogins(); + + await promptDone; + is(logins.length, 3, "expected number of logins"); + + ok(await isLoggedIn(), "should be logged in after MP prompt"); + logoutPrimaryPassword(); + ok(!await isLoggedIn(), "should be logged out"); +}); + +add_task(async function test_2() { + // Try again but click cancel. + const state = { + msg: "Please enter your Primary Password.", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + const action = { + buttonClick: "cancel", + }; + const promptDone = handlePrompt(state, action); + + const logins = await LoginManager.getAllLogins().catch(() => {}); + await promptDone; + is(logins, undefined, "shouldn't have gotten logins"); + ok(!await isLoggedIn(), "should still be logged out"); +}); + +add_task(async function test_3() { + const state = { + msg: "Please enter your Primary Password.", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + const action = { + buttonClick: "ok", + passField: LoginTestUtils.primaryPassword.primaryPassword, + }; + const promptDone = handlePrompt(state, action); + + const fillPromise = promiseFormsProcessed(); + + info("Load a single window to trigger a MP"); + await SimpleTest.promiseFocus(win, true); + win.location = exampleCom + "subtst_primary_pass.html"; + + await promptDone; + info("promptDone"); + await fillPromise; + info("filled"); + + // check contents of win fields + + await SpecialPowers.spawn(win, [], function() { + const u = this.content.document.getElementById("userfield"); + const p = this.content.document.getElementById("passfield"); + Assert.equal(u.value, "user1", "checking expected user to have been filled in"); + Assert.equal(p.value, "pass1", "checking expected pass to have been filled in"); + u.value = ""; + p.value = ""; + }); + + ok(await isLoggedIn(), "should be logged in"); + logoutPrimaryPassword(); + ok(!await isLoggedIn(), "should be logged out"); +}); + +add_task(async function test_4() { + const state = { + msg: "Please enter your Primary Password.", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + let action = { + buttonClick: "none", + }; + const promptDone = handlePrompt(state, action); + + // first part of loading 2 MP-triggering windows + await SimpleTest.promiseFocus(win); + win.location = exampleOrg + "subtst_primary_pass.html"; + // The MP prompt is open but don't take any action yet. + await promptDone; + + // check contents of win fields + await SpecialPowers.spawn(win, [], function() { + const u = this.content.document.getElementById("userfield"); + const p = this.content.document.getElementById("passfield"); + Assert.equal(u.value, "", "checking expected empty user"); + Assert.equal(p.value, "", "checking expected empty pass"); + }); + + ok(!await isLoggedIn(), "should be logged out"); + + // XXX check that there's 1 MP window open + + // Load a second login form in an iframe + // This should detect that there's already a pending MP prompt, and not + // put up a second one. + + // Since the Primary Password prompt is open, we can't focus another tab + // to load the second form. Instead, we load the same form into an iframe. + const url = exampleOrg + "subtst_primary_pass.html"; + await SpecialPowers.spawn(win, [url], async function(urlF) { + const iframe = this.content.document.querySelector("iframe"); + const loadPromise = new Promise(resolve => { + iframe.addEventListener("load", function onload() { + resolve(); + }, { once: true }); + }); + // Use the same origin as the top level to ensure we would autofill + // if we could (we don't fill in cross-origin iframes). + iframe.src = urlF; + await loadPromise; + }); + + // We can't use promiseFormsProcessed* here, because _fillForm doesn't + // run if Primary Password is locked. + await new Promise(resolve => { + // Testing a negative, wait a little to give the login manager a chance to + // (incorrectly) fill in the form. Note, we cannot use setTimeout() + // here because the modal window suspends all window timers. Instead we + // must use a chrome script to use nsITimer directly. + const chromeURL = SimpleTest.getTestFileURL("chrome_timeout.js"); + const script = SpecialPowers.loadChromeScript(chromeURL); + script.addMessageListener("ready", _ => { + script.sendAsyncMessage("setTimeout", { delay: 500 }); + }); + script.addMessageListener("timeout", resolve); + }); + + // iframe should load without having triggered a MP prompt (because one + // is already waiting) + + // check contents of iframe fields + await SpecialPowers.spawn(win, [], function() { + const iframe = this.content.document.querySelector("iframe"); + const frameDoc = iframe.contentDocument; + const u = frameDoc.getElementById("userfield"); + const p = frameDoc.getElementById("passfield"); + Assert.equal(u.value, "", "checking expected empty user"); + Assert.equal(p.value, "", "checking expected empty pass"); + }); + + // XXX check that there's 1 MP window open + ok(!await isLoggedIn(), "should be logged out"); + + // Ok, now enter the MP. The MP prompt is already up. + const fillPromise = promiseFormsProcessed(2); + + // fill existing MP dialog with MP. + action = { + buttonClick: "ok", + passField: LoginTestUtils.primaryPassword.primaryPassword, + }; + await handlePrompt(state, action); + await fillPromise; + + // We shouldn't have to worry about win's load event racing with + // filling of the iframe's data. We notify observers synchronously, so + // the iframe's observer will process the iframe before win even finishes + // processing the form. + ok(await isLoggedIn(), "should be logged in"); + + // check contents of win fields + await SpecialPowers.spawn(win, [], function() { + const u = this.content.document.getElementById("userfield"); + const p = this.content.document.getElementById("passfield"); + Assert.equal(u.value, "user2", "checking expected user to have been filled in"); + Assert.equal(p.value, "pass2", "checking expected pass to have been filled in"); + + // clearing fields to not cause a submission when the next document is loaded + u.value = ""; + p.value = ""; + }); + + // check contents of iframe fields + await SpecialPowers.spawn(win, [], function() { + const iframe = this.content.document.querySelector("iframe"); + const frameDoc = iframe.contentDocument; + const u = frameDoc.getElementById("userfield"); + const p = frameDoc.getElementById("passfield"); + Assert.equal(u.value, "user2", "checking expected user to have been filled in"); + Assert.equal(p.value, "pass2", "checking expected pass to have been filled in"); + + // clearing fields to not cause a submission when the next document is loaded + u.value = ""; + p.value = ""; + }); +}); + +// XXX do a test5ABC with clicking cancel? + +SimpleTest.registerCleanupFunction(function finishTest() { + disablePrimaryPassword(); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html new file mode 100644 index 0000000000..12d3a7d080 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt.html @@ -0,0 +1,669 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test prompter.{prompt,asyncPromptPassword,asyncPromptUsernameAndPassword}</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +var state, action; +var uname = { value: null }; +var pword = { value: null }; +var result = { value: null }; +var isOk; + +// Force parent to not look for tab-modal prompts, as they're not used for auth prompts. +modalType = Ci.nsIPrompt.MODAL_TYPE_WINDOW; + +let prompterParent = runInParent(() => { + const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"]. + getService(Ci.nsIPromptFactory); + + let chromeWin = Services.wm.getMostRecentWindow("navigator:browser"); + let prompter1 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt); + + addMessageListener("proxyPrompter", async function onMessage(msg) { + let rv = await prompter1[msg.methodName](...msg.args); + return { + rv, + // Send the args back to content so out/inout args can be checked. + args: msg.args, + }; + }); +}); + +let prompter1 = new PrompterProxy(prompterParent); + +const defaultTitle = "the title"; +const defaultMsg = "the message"; + +add_setup(async () => { + await setStoredLoginsAsync( + ["http://example.com", null, "http://example.com", "", "examplepass", "", ""], + ["http://example2.com", null, "http://example2.com", "user1name", "user1pass", "", ""], + ["http://example2.com", null, "http://example2.com", "user2name", "user2pass", "", ""], + ["http://example2.com", null, "http://example2.com", "user3.name@host", "user3pass", "", ""], + ["http://example2.com", null, "http://example2.com", "100@beef", "user3pass", "", ""], + ["http://example2.com", null, "http://example2.com", "100%beef", "user3pass", "", ""] + ); +}); + +add_task(async function test_prompt_accept() { + state = { + msg: "the message", + title: "the title", + textValue: "abc", + passValue: "", + iconClass: "question-icon", + titleHidden: true, + textHidden: false, + passHidden: true, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + textField: "xyz", + }; + promptDone = handlePrompt(state, action); + isOk = prompter1.prompt(defaultTitle, defaultMsg, "http://example.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, "abc", result); + await promptDone; + + ok(isOk, "Checking dialog return value (accept)"); + is(result.value, "xyz", "Checking prompt() returned value"); +}); + +add_task(async function test_prompt_cancel() { + state = { + msg: "the message", + title: "the title", + textValue: "abc", + passValue: "", + iconClass: "question-icon", + titleHidden: true, + textHidden: false, + passHidden: true, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "cancel", + }; + promptDone = handlePrompt(state, action); + isOk = prompter1.prompt(defaultTitle, defaultMsg, "http://example.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, "abc", result); + await promptDone; + ok(!isOk, "Checking dialog return value (cancel)"); +}); + +add_task(async function test_promptPassword_defaultAccept() { + // Default password provided, existing logins are ignored. + state = { + msg: "the message", + title: "the title", + textValue: "", + passValue: "inputpw", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + passField: "secret", + }; + pword.value = "inputpw"; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://example.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "secret", "Checking returned password"); +}); + +add_task(async function test_promptPassword_defaultCancel() { + // Default password provided, existing logins are ignored. + state = { + msg: "the message", + title: "the title", + textValue: "", + passValue: "inputpw", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + action = { + buttonClick: "cancel", + }; + pword.value = "inputpw"; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://example.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + await promptDone; + ok(!isOk, "Checking dialog return value (cancel)"); +}); + +add_task(async function test_promptPassword_emptyAccept() { + // No default password provided, realm does not match existing login. + state = { + msg: "the message", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + passField: "secret", + }; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://nonexample.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "secret", "Checking returned password"); +}); + +add_task(async function test_promptPassword_saved() { + // No default password provided, matching login is returned w/o prompting. + pword.value = null; + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://example.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "examplepass", "Checking returned password"); +}); + +add_task(async function test_promptPassword_noMatchingPasswordForEmptyUN() { + // No default password provided, none of the logins from this host are + // password-only so the user is prompted. + state = { + msg: "the message", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + passField: "secret", + }; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "secret", "Checking returned password"); +}); + +add_task(async function test_promptPassword_matchingPWForUN() { + // No default password provided, matching login is returned w/o prompting. + pword.value = null; + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://user1name@example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "user1pass", "Checking returned password"); +}); + +add_task(async function test_promptPassword_matchingPWForUN2() { + // No default password provided, matching login is returned w/o prompting. + pword.value = null; + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://user2name@example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "user2pass", "Checking returned password"); +}); + +add_task(async function test_promptPassword_matchingPWForUN3() { + // No default password provided, matching login is returned w/o prompting. + pword.value = null; + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://user3%2Ename%40host@example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "user3pass", "Checking returned password"); +}); + +add_task(async function test_promptPassword_extraAt() { + // No default password provided, matching login is returned w/o prompting. + pword.value = null; + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://100@beef@example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "user3pass", "Checking returned password"); +}); + +add_task(async function test_promptPassword_usernameEncoding() { + // No default password provided, matching login is returned w/o prompting. + pword.value = null; + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "http://100%25beef@example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "user3pass", "Checking returned password"); + + // XXX test saving a password with Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY +}); + +add_task(async function test_promptPassword_realm() { + // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt + state = { + msg: "the message", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + passField: "fill2pass", + }; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "fill2pass", "Checking returned password"); +}); + +add_task(async function test_promptPassword_realm2() { + // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt + state = { + msg: "the message", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: true, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "passField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + passField: "fill2pass", + }; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)", + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(pword.value, "fill2pass", "Checking returned password"); +}); + +add_task(async function test_promptUsernameAndPassword_accept() { + state = { + msg: "the message", + title: "the title", + textValue: "inuser", + passValue: "inpass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + textField: "outuser", + passField: "outpass", + }; + uname.value = "inuser"; + pword.value = "inpass"; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://nonexample.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(uname.value, "outuser", "Checking returned username"); + is(pword.value, "outpass", "Checking returned password"); +}); + +add_task(async function test_promptUsernameAndPassword_cancel() { + state = { + msg: "the message", + title: "the title", + textValue: "inuser", + passValue: "inpass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "cancel", + }; + uname.value = "inuser"; + pword.value = "inpass"; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://nonexample.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword); + await promptDone; + ok(!isOk, "Checking dialog return value (cancel)"); +}); + +add_task(async function test_promptUsernameAndPassword_autofill() { + // test filling in existing password-only login + state = { + msg: "the message", + title: "the title", + textValue: "", + passValue: "examplepass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + }; + uname.value = null; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(uname.value, "", "Checking returned username"); + is(pword.value, "examplepass", "Checking returned password"); +}); + +add_task(async function test_promptUsernameAndPassword_multipleExisting() { + // test filling in existing login (undetermined from multiple selection) + // user2name/user2pass would also be valid to fill here. + state = { + msg: "the message", + title: "the title", + textValue: "user1name", + passValue: "user1pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + }; + uname.value = null; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + ok(uname.value == "user1name" || uname.value == "user2name", "Checking returned username"); + ok(pword.value == "user1pass" || uname.value == "user2pass", "Checking returned password"); +}); + +add_task(async function test_promptUsernameAndPassword_multipleExisting1() { + // test filling in existing login (user1 from multiple selection) + state = { + msg: "the message", + title: "the title", + textValue: "user1name", + passValue: "user1pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + }; + uname.value = "user1name"; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(uname.value, "user1name", "Checking returned username"); + is(pword.value, "user1pass", "Checking returned password"); +}); + +add_task(async function test_promptUsernameAndPassword_multipleExisting2() { + // test filling in existing login (user2 from multiple selection) + state = { + msg: "the message", + title: "the title", + textValue: "user2name", + passValue: "user2pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + }; + uname.value = "user2name"; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(uname.value, "user2name", "Checking returned username"); + is(pword.value, "user2pass", "Checking returned password"); +}); + +add_task(async function test_promptUsernameAndPassword_passwordChange() { + // test changing password + state = { + msg: "the message", + title: "the title", + textValue: "user2name", + passValue: "user2pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + passField: "NEWuser2pass", + }; + uname.value = "user2name"; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(uname.value, "user2name", "Checking returned username"); + is(pword.value, "NEWuser2pass", "Checking returned password"); +}); + +add_task(async function test_promptUsernameAndPassword_changePasswordBack() { + // test changing password (back to original value) + state = { + msg: "the message", + title: "the title", + textValue: "user2name", + passValue: "NEWuser2pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + passField: "user2pass", + }; + uname.value = "user2name"; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "http://example2.com", + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(uname.value, "user2name", "Checking returned username"); + is(pword.value, "user2pass", "Checking returned password"); +}); + +add_task(async function test_promptUsernameAndPassword_realm() { + // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt + state = { + msg: "the message", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + textField: "fill2user", + passField: "fill2pass", + }; + uname.value = null; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)", + Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER, uname, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(uname.value, "fill2user", "Checking returned username"); + is(pword.value, "fill2pass", "Checking returned password"); +}); + +add_task(async function test_promptUsernameAndPassword_realm2() { + // We don't pre-fill or save for NS_GetAuthKey-generated realms, but we should still prompt + state = { + msg: "the message", + title: "the title", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + textField: "fill2user", + passField: "fill2pass", + }; + uname.value = null; + pword.value = null; + promptDone = handlePrompt(state, action); + isOk = await prompter1.asyncPromptUsernameAndPassword(defaultTitle, defaultMsg, "example2.com:80 (somerealm)", + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, uname, pword); + await promptDone; + ok(isOk, "Checking dialog return value (accept)"); + is(uname.value, "fill2user", "Checking returned username"); + is(pword.value, "fill2pass", "Checking returned password"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html new file mode 100644 index 0000000000..41a58cb416 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_async.html @@ -0,0 +1,621 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Async Auth Prompt</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <script class="testbody" type="text/javascript"> + const { NetUtil } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" + ); + const { TestUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" + ); + const EXAMPLE_COM = "http://example.com/tests/toolkit/components/passwordmgr/test/mochitest/"; + const EXAMPLE_ORG = "http://example.org/tests/toolkit/components/passwordmgr/test/mochitest/"; + let mozproxyOrigin; + + // Let prompt_common know what kind of modal type is enabled for auth prompts. + modalType = authPromptModalType; + + // These are magically defined on the window due to the iframe IDs + /* global iframe1, iframe2a, iframe2b */ + + /** + * Add a listener to add some logins to be autofilled in the HTTP/proxy auth. prompts later. + */ + let pwmgrParent = runInParent(() => { + Services.prefs.setIntPref("network.auth.subresource-http-auth-allow", 2); + Services.prefs.setIntPref("prompts.authentication_dialog_abuse_limit", -1); + + addMessageListener("initLogins", async function onMessage(msg) { + const loginsData = [ + [msg.mozproxyOrigin, "proxy_realm", "proxy_user", "proxy_pass"], + [msg.mozproxyOrigin, "proxy_realm2", "proxy_user2", "proxy_pass2"], + [msg.mozproxyOrigin, "proxy_realm3", "proxy_user3", "proxy_pass3"], + [msg.mozproxyOrigin, "proxy_realm4", "proxy_user4", "proxy_pass4"], + [msg.mozproxyOrigin, "proxy_realm5", "proxy_user5", "proxy_pass5"], + ["http://example.com", "mochirealm", "user1name", "user1pass"], + ["http://example.org", "mochirealm2", "user2name", "user2pass"], + ["http://example.com", "mochirealm3", "user3name", "user3pass"], + ["http://example.com", "mochirealm4", "user4name", "user4pass"], + ["http://example.com", "mochirealm5", "user5name", "user5pass"], + ["http://example.com", "mochirealm6", "user6name", "user6pass"] + ]; + const logins = loginsData.map(([host, realm, user, pass]) => { + const login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo); + login.init(host, null, realm, user, pass, "", ""); + return login + }) + await Services.logins.addLogins(logins); + }); + }); // end runInParent + + function promiseLoadedContentDoc(frame) { + return new Promise(resolve => { + frame.addEventListener("load", function onLoad(evt) { + resolve(SpecialPowers.wrap(frame).contentDocument); + }, { once: true }); + }); + } + + function promiseProxyErrorLoad(frame) { + return TestUtils.waitForCondition(async function checkForProxyConnectFailure() { + try { + return await SpecialPowers.spawn(frame, [], function() { + return this.content.document.documentURI.includes("proxyConnectFailure"); + }) + } catch (e) { + // The frame may not be ready for the 'spawn' task right after setting + // iframe.src, which will throw an exception when that happens. + // Since this test is testing error load, we can't wait until the iframe + // is 'loaded' either. So we simply catch the exception here and retry the task + // later since we are in the waitForCondition loop. + return false; + } + }, "Waiting for proxyConnectFailure documentURI"); + } + + /** + * Make a channel to get the ProxyInfo used by the test harness so that we + * can add logins for the correct proxy origin. + */ + add_task(async function setup_getProxyInfoForHarness() { + await new Promise(resolve => { + let resolveCallback = SpecialPowers.wrapCallbackObject({ + // eslint-disable-next-line mozilla/use-chromeutils-generateqi + QueryInterface(iid) { + const interfaces = [Ci.nsIProtocolProxyCallback, Ci.nsISupports]; + + if (!interfaces.some(v => iid.equals(v))) { + throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE; + } + return this; + }, + + onProxyAvailable(req, uri, pi, status) { + // Add logins using the proxy host and port used by the mochitest harness. + mozproxyOrigin = "moz-proxy://" + SpecialPowers.wrap(pi).host + ":" + + SpecialPowers.wrap(pi).port; + + pwmgrParent.sendQuery("initLogins", {mozproxyOrigin}).then(resolve) + }, + }); + + // Need to allow for arbitrary network servers defined in PAC instead of a hardcoded moz-proxy. + let channel = NetUtil.newChannel({ + uri: "http://example.com", + loadUsingSystemPrincipal: true, + }); + + let pps = SpecialPowers.Cc["@mozilla.org/network/protocol-proxy-service;1"] + .getService(); + + pps.asyncResolve(channel, 0, resolveCallback); + }); + }); + + add_task(async function test_proxyAuthThenTwoHTTPAuth() { + // Load through a single proxy with authentication required 3 different + // pages, first with one login, other two with their own different login. + // We expect to show just a single dialog for proxy authentication and + // then two dialogs to authenticate to login 1 and then login 2. + + let iframe1DocPromise = promiseLoadedContentDoc(iframe1); + let iframe2aDocPromise = promiseLoadedContentDoc(iframe2a); + let iframe2bDocPromise = promiseLoadedContentDoc(iframe2b); + + iframe1.src = EXAMPLE_COM + "authenticate.sjs?" + + "r=1&" + + "user=user1name&" + + "pass=user1pass&" + + "realm=mochirealm&" + + "proxy_user=proxy_user&" + + "proxy_pass=proxy_pass&" + + "proxy_realm=proxy_realm"; + iframe2a.src = EXAMPLE_ORG + "authenticate.sjs?" + + "r=2&" + + "user=user2name&" + + "pass=user2pass&" + + "realm=mochirealm2&" + + "proxy_user=proxy_user&" + + "proxy_pass=proxy_pass&" + + "proxy_realm=proxy_realm"; + iframe2b.src = EXAMPLE_ORG + "authenticate.sjs?" + + "r=3&" + + "user=user2name&" + + "pass=user2pass&" + + "realm=mochirealm2&" + + "proxy_user=proxy_user&" + + "proxy_pass=proxy_pass&" + + "proxy_realm=proxy_realm"; + + let state = { + msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm”`, + title: "Authentication Required", + textValue: "proxy_user", + passValue: "proxy_pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + await handlePrompt(state, action); + + // We don't know what order these prompts appear in so get both states and check them. + // We can't use Promise.all here since we can't start the 2nd timer in chromeScript.js until + // the first timer is done since the timer variable gets clobbered, plus we don't want + // different actions racing each other. + let promptStates = [ + await handlePromptWithoutChecks(action), + await handlePromptWithoutChecks(action), + ]; + + let expected1 = Object.assign({}, state, { + msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.", + textValue: "user1name", + passValue: "user1pass", + }); + + let expected2 = Object.assign({}, state, { + msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.org, not the website you are currently visiting.", + textValue: "user2name", + passValue: "user2pass", + }); + + // The order isn't important. + let expectedPromptStates = [ + expected1, + expected2, + ]; + + is(promptStates.length, expectedPromptStates.length, + "Check we handled the right number of prompts"); + for (let promptState of promptStates) { + let expectedStateIndexForMessage = expectedPromptStates.findIndex(eps => { + return eps.msg == promptState.msg; + }); + isnot(expectedStateIndexForMessage, -1, "Check state message was found in expected array"); + let expectedPromptState = expectedPromptStates.splice(expectedStateIndexForMessage, 1)[0]; + checkPromptState(promptState, expectedPromptState); + } + + await iframe1DocPromise; + await iframe2aDocPromise; + await iframe2bDocPromise; + + await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() { + let doc = this.content.document; + let authok1 = doc.getElementById("ok").textContent; + let proxyok1 = doc.getElementById("proxy").textContent; + Assert.equal(authok1, "PASS", "WWW Authorization OK, frame1"); + Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1"); + }); + + await SpecialPowers.spawn(getIframeBrowsingContext(window, 1), [], function() { + let doc = this.content.document; + let authok2a = doc.getElementById("ok").textContent; + let proxyok2a = doc.getElementById("proxy").textContent; + Assert.equal(authok2a, "PASS", "WWW Authorization OK, frame2a"); + Assert.equal(proxyok2a, "PASS", "Proxy Authorization OK, frame2a"); + }); + + await SpecialPowers.spawn(getIframeBrowsingContext(window, 2), [], function() { + let doc = this.content.document; + let authok2b = doc.getElementById("ok").textContent; + let proxyok2b = doc.getElementById("proxy").textContent; + Assert.equal(authok2b, "PASS", "WWW Authorization OK, frame2b"); + Assert.equal(proxyok2b, "PASS", "Proxy Authorization OK, frame2b"); + }); + }); + + add_task(async function test_threeSubframesWithSameProxyAndHTTPAuth() { + // Load an iframe with 3 subpages all requiring the same login through + // an authenticated proxy. We expect 2 dialogs, proxy authentication + // and web authentication. + + let iframe1DocPromise = promiseLoadedContentDoc(iframe1); + + iframe1.src = EXAMPLE_COM + "subtst_prompt_async.html"; + iframe2a.src = "about:blank"; + iframe2b.src = "about:blank"; + + let state = { + msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm2”`, + title: "Authentication Required", + textValue: "proxy_user2", + passValue: "proxy_pass2", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + await handlePrompt(state, action); + + Object.assign(state, { + msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.", + textValue: "user3name", + passValue: "user3pass", + }); + await handlePrompt(state, action); + + await iframe1DocPromise; + + function checkIframe(frameid) { + let doc = this.content.document; + let authok = doc.getElementById("ok").textContent; + let proxyok = doc.getElementById("proxy").textContent; + + Assert.equal(authok, "PASS", "WWW Authorization OK, " + frameid); + Assert.equal(proxyok, "PASS", "Proxy Authorization OK, " + frameid); + } + + let parentIFrameBC = SpecialPowers.wrap(window).windowGlobalChild + .browsingContext.children[0]; + + let childIFrame = SpecialPowers.unwrap(parentIFrameBC.children[0]); + await SpecialPowers.spawn(childIFrame, ["iframe1"], checkIframe); + childIFrame = SpecialPowers.unwrap(parentIFrameBC.children[1]); + await SpecialPowers.spawn(childIFrame, ["iframe2"], checkIframe); + childIFrame = SpecialPowers.unwrap(parentIFrameBC.children[2]); + await SpecialPowers.spawn(childIFrame, ["iframe3"], checkIframe); + }); + + add_task(async function test_oneFrameWithUnauthenticatedProxy() { + // Load in the iframe page through unauthenticated proxy + // and discard the proxy authentication. We expect to see + // unauthenticated page content and just a single dialog. + + iframe1.src = EXAMPLE_COM + "authenticate.sjs?" + + "user=user4name&" + + "pass=user4pass&" + + "realm=mochirealm4&" + + "proxy_user=proxy_user3&" + + "proxy_pass=proxy_pass3&" + + "proxy_realm=proxy_realm3"; + + let state = { + msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm3”`, + title: "Authentication Required", + textValue: "proxy_user3", + passValue: "proxy_pass3", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "cancel", + }; + await handlePrompt(state, action); + + await promiseProxyErrorLoad(iframe1); + }); + + add_task(async function test_reloadReusingProxyAuthButCancellingHTTPAuth() { + // Reload the frame from previous step and pass the proxy authentication + // but cancel the WWW authentication. We should get the proxy=ok and WWW=fail + // content as a result. + let iframe1DocPromise = promiseLoadedContentDoc(iframe1); + + iframe1.src = EXAMPLE_COM + "authenticate.sjs?" + + "user=user4name&" + + "pass=user4pass&" + + "realm=mochirealm4&" + + "proxy_user=proxy_user3&" + + "proxy_pass=proxy_pass3&" + + "proxy_realm=proxy_realm3"; + + let state = { + msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm3”`, + title: "Authentication Required", + textValue: "proxy_user3", + passValue: "proxy_pass3", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + await handlePrompt(state, action); + + Object.assign(state, { + msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.", + textValue: "user4name", + passValue: "user4pass", + }); + action = { + buttonClick: "cancel", + }; + await handlePrompt(state, action); + + await iframe1DocPromise; + + await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() { + let doc = this.content.document; + let authok1 = doc.getElementById("ok").textContent; + let proxyok1 = doc.getElementById("proxy").textContent; + + Assert.equal(authok1, "FAIL", "WWW Authorization FAILED, frame1"); + Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1"); + }); + }); + + add_task(async function test_hugePayloadCancelled() { + // Same as the previous two steps but let the server generate + // huge content load to check http channel is capable to handle + // case when auth dialog is canceled or accepted before unauthenticated + // content data is load from the server. (This would be better to + // implement using delay of server response). + iframe1.src = EXAMPLE_COM + "authenticate.sjs?" + + "user=user5name&" + + "pass=user5pass&" + + "realm=mochirealm5&" + + "proxy_user=proxy_user4&" + + "proxy_pass=proxy_pass4&" + + "proxy_realm=proxy_realm4&" + + "huge=1"; + + let state = { + msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm4”`, + title: "Authentication Required", + textValue: "proxy_user4", + passValue: "proxy_pass4", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "cancel", + }; + await handlePrompt(state, action); + + await promiseProxyErrorLoad(iframe1); + }); + + add_task(async function test_hugeProxySuccessWWWFail() { + // Reload the frame from the previous step and let the proxy + // authentication pass but WWW fail. We expect two dialogs + // and an unauthenticated page content load. + + let iframe1DocPromise = promiseLoadedContentDoc(iframe1); + iframe1.src = EXAMPLE_COM + "authenticate.sjs?" + + "user=user5name&" + + "pass=user5pass&" + + "realm=mochirealm5&" + + "proxy_user=proxy_user4&" + + "proxy_pass=proxy_pass4&" + + "proxy_realm=proxy_realm4&" + + "huge=1"; + + let state = { + msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm4”`, + title: "Authentication Required", + textValue: "proxy_user4", + passValue: "proxy_pass4", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + await handlePrompt(state, action); + + Object.assign(state, { + msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.", + textValue: "user5name", + passValue: "user5pass", + }); + action = { + buttonClick: "cancel", + }; + await handlePrompt(state, action); + + await iframe1DocPromise; + + await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() { + let doc = this.content.document; + let authok1 = doc.getElementById("ok").textContent; + let proxyok1 = doc.getElementById("proxy").textContent; + let footnote = doc.getElementById("footnote").textContent; + + Assert.equal(authok1, "FAIL", "WWW Authorization FAILED, frame1"); + Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1"); + Assert.equal(footnote, "This is a footnote after the huge content fill", + "Footnote present and loaded completely"); + }); + }); + + add_task(async function test_hugeProxySuccessWWWSuccess() { + // Reload again and let pass all authentication dialogs. + // Check we get the authenticated content not broken by + // the unauthenticated content. + + let iframe1DocPromise = promiseLoadedContentDoc(iframe1); + await SpecialPowers.spawn(iframe1, [], function() { + this.content.document.location.reload(); + }); + + let state = { + msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.", + title: "Authentication Required", + textValue: "user5name", + passValue: "user5pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + await handlePrompt(state, action); + + await iframe1DocPromise; + + await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() { + let doc = this.content.document; + let authok1 = doc.getElementById("ok").textContent; + let proxyok1 = doc.getElementById("proxy").textContent; + let footnote = doc.getElementById("footnote").textContent; + + Assert.equal(authok1, "PASS", "WWW Authorization OK, frame1"); + Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1"); + Assert.equal(footnote, "This is a footnote after the huge content fill", + "Footnote present and loaded completely"); + }); + }); + + add_task(async function test_cancelSome() { + // Check we process all challenges sent by server when + // user cancels prompts + let iframe1DocPromise = promiseLoadedContentDoc(iframe1); + iframe1.src = EXAMPLE_COM + "authenticate.sjs?" + + "user=user6name&" + + "pass=user6pass&" + + "realm=mochirealm6&" + + "proxy_user=proxy_user5&" + + "proxy_pass=proxy_pass5&" + + "proxy_realm=proxy_realm5&" + + "huge=1&" + + "multiple=3"; + + let state = { + msg: `The proxy ${mozproxyOrigin} is requesting a username and password. The site says: “proxy_realm5”`, + title: "Authentication Required", + textValue: "proxy_user5", + passValue: "proxy_pass5", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "cancel", + }; + await handlePrompt(state, action); + + action = { + buttonClick: "cancel", + }; + await handlePrompt(state, action); + + action = { + buttonClick: "ok", + }; + await handlePrompt(state, action); + + Object.assign(state, { + msg: "This site is asking you to sign in. Warning: Your login information will be shared with example.com, not the website you are currently visiting.", + textValue: "user6name", + passValue: "user6pass", + }); + + action = { + buttonClick: "cancel", + }; + await handlePrompt(state, action); + + action = { + buttonClick: "ok", + }; + await handlePrompt(state, action); + + await iframe1DocPromise; + await SpecialPowers.spawn(getIframeBrowsingContext(window, 0), [], function() { + let doc = this.content.document; + let authok1 = doc.getElementById("ok").textContent; + let proxyok1 = doc.getElementById("proxy").textContent; + let footnote = doc.getElementById("footnote").textContent; + + Assert.equal(authok1, "PASS", "WWW Authorization OK, frame1"); + Assert.equal(proxyok1, "PASS", "Proxy Authorization OK, frame1"); + Assert.equal(footnote, "This is a footnote after the huge content fill", + "Footnote present and loaded completely"); + }); + + }); + </script> +</head> +<body> + <iframe id="iframe1"></iframe> + <iframe id="iframe2a"></iframe> + <iframe id="iframe2b"></iframe> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html new file mode 100644 index 0000000000..f6f6e681dc --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_http.html @@ -0,0 +1,319 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test HTTP auth prompts by loading authenticate.sjs</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content" style="display: none"> + <iframe id="iframe"></iframe> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +var iframe = document.getElementById("iframe"); + +// Let prompt_common know what kind of modal type is enabled for auth prompts. +modalType = authPromptModalType; + +const AUTHENTICATE_PATH = new URL("authenticate.sjs", window.location.href).pathname; + +add_setup(async () => { + await setStoredLoginsAsync( + ["http://mochi.test:8888", null, "mochitest", "mochiuser1", "mochipass1", "", ""], + ["http://mochi.test:8888", null, "mochitest2", "mochiuser2", "mochipass2", "", ""], + ["http://mochi.test:8888", null, "mochitest3", "mochiuser3", "mochipass3-old", "", ""], + // Logins to test scheme upgrades (allowed) and downgrades (disallowed) + ["http://example.com", null, "schemeUpgrade", "httpUser", "httpPass", "", ""], + ["https://example.com", null, "schemeDowngrade", "httpsUser", "httpsPass", "", ""], + // HTTP and HTTPS version of the same domain and realm but with different passwords. + ["http://example.org", null, "schemeUpgradeDedupe", "dedupeUser", "httpPass", "", ""], + ["https://example.org", null, "schemeUpgradeDedupe", "dedupeUser", "httpsPass", "", ""] + ); +}); + +add_task(async function test_iframe() { + let state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "mochiuser1", + passValue: "mochipass1", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + promptDone = handlePrompt(state, action); + + // The following tests are driven by iframe loads + + var iframeLoaded = onloadPromiseFor("iframe"); + iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1"; + await promptDone; + await iframeLoaded; + await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"}, + iframe); + + state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "mochiuser2", + passValue: "mochipass2", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + }; + promptDone = handlePrompt(state, action); + // We've already authenticated to this host:port. For this next + // request, the existing auth should be sent, we'll get a 401 reply, + // and we should prompt for new auth. + iframeLoaded = onloadPromiseFor("iframe"); + iframe.src = "authenticate.sjs?user=mochiuser2&pass=mochipass2&realm=mochitest2"; + await promptDone; + await iframeLoaded; + await checkEchoedAuthInfo({user: "mochiuser2", pass: "mochipass2"}, + iframe); + + // Now make a load that requests the realm from test 1000. It was + // already provided there, so auth will *not* be prompted for -- the + // networking layer already knows it! + iframeLoaded = onloadPromiseFor("iframe"); + iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1"; + await iframeLoaded; + await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1"}, + iframe); + + // Same realm we've already authenticated to, but with a different + // expected password (to trigger an auth prompt, and change-password + // popup notification). + state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "mochiuser1", + passValue: "mochipass1", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + passField: "mochipass1-new", + }; + promptDone = handlePrompt(state, action); + iframeLoaded = onloadPromiseFor("iframe"); + let promptShownPromise = promisePromptShown("passwordmgr-prompt-change"); + iframe.src = "authenticate.sjs?user=mochiuser1&pass=mochipass1-new"; + await promptDone; + await iframeLoaded; + await checkEchoedAuthInfo({user: "mochiuser1", pass: "mochipass1-new"}, + iframe); + await promptShownPromise; + + // Same as last test, but for a realm we haven't already authenticated + // to (but have an existing saved login for, so that we'll trigger + // a change-password popup notification. + state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "mochiuser3", + passValue: "mochipass3-old", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + passField: "mochipass3-new", + }; + promptDone = handlePrompt(state, action); + iframeLoaded = onloadPromiseFor("iframe"); + promptShownPromise = promisePromptShown("passwordmgr-prompt-change"); + iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-new&realm=mochitest3"; + await promptDone; + await iframeLoaded; + await checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-new"}, + iframe); + await promptShownPromise; + + // Housekeeping: Delete login4 to test the save prompt in the next test. + runInParent(() => { + var tmpLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. + createInstance(Ci.nsILoginInfo); + tmpLogin.init("http://mochi.test:8888", null, "mochitest3", + "mochiuser3", "mochipass3-old", "", ""); + Services.logins.removeLogin(tmpLogin); + + // Clear cached auth from this subtest, and avoid leaking due to bug 459620. + var authMgr = Cc["@mozilla.org/network/http-auth-manager;1"]. + getService(Ci.nsIHttpAuthManager); + authMgr.clearAll(); + }); + + state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "", + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + action = { + buttonClick: "ok", + textField: "mochiuser3", + passField: "mochipass3-old", + }; + // Trigger a new prompt, so we can test adding a new login. + promptDone = handlePrompt(state, action); + + iframeLoaded = onloadPromiseFor("iframe"); + promptShownPromise = promisePromptShown("passwordmgr-prompt-save"); + iframe.src = "authenticate.sjs?user=mochiuser3&pass=mochipass3-old&realm=mochitest3"; + await promptDone; + await iframeLoaded; + await checkEchoedAuthInfo({user: "mochiuser3", pass: "mochipass3-old"}, + iframe); + await promptShownPromise; +}); + +add_task(async function test_schemeUpgrade() { + let state = { + msg: "This site is asking you to sign in. Warning: Your login information " + + "will be shared with example.com, not the website you are currently visiting.", + title: "Authentication Required", + textValue: "httpUser", + passValue: "httpPass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + let promptDone = handlePrompt(state, action); + + // The following tests are driven by iframe loads + + let iframeLoaded = onloadPromiseFor("iframe"); + iframe.src = "https://example.com" + AUTHENTICATE_PATH + + "?user=httpUser&pass=httpPass&realm=schemeUpgrade"; + await promptDone; + await iframeLoaded; + await checkEchoedAuthInfo({user: "httpUser", pass: "httpPass"}, + iframe); +}); + +add_task(async function test_schemeDowngrade() { + const state = { + msg: "This site is asking you to sign in. Warning: Your login information " + + "will be shared with example.com, not the website you are currently visiting.", + title: "Authentication Required", + textValue: "", // empty because we shouldn't downgrade + passValue: "", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + const action = { + buttonClick: "cancel", + }; + const promptDone = handlePrompt(state, action); + + // The following tests are driven by iframe loads + + const iframeLoaded = onloadPromiseFor("iframe"); + iframe.src = "http://example.com" + AUTHENTICATE_PATH + + "?user=unused&pass=unused&realm=schemeDowngrade"; + await promptDone; + await iframeLoaded; +}); + +add_task(async function test_schemeUpgrade_dedupe() { + const state = { + msg: "This site is asking you to sign in. Warning: Your login information " + + "will be shared with example.org, not the website you are currently visiting.", + title: "Authentication Required", + textValue: "dedupeUser", + passValue: "httpsPass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + const action = { + buttonClick: "ok", + }; + const promptDone = handlePrompt(state, action); + + // The following tests are driven by iframe loads + + const iframeLoaded = onloadPromiseFor("iframe"); + iframe.src = "https://example.org" + AUTHENTICATE_PATH + + "?user=dedupeUser&pass=httpsPass&realm=schemeUpgradeDedupe"; + await promptDone; + await iframeLoaded; + await checkEchoedAuthInfo({user: "dedupeUser", pass: "httpsPass"}, + iframe); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html new file mode 100644 index 0000000000..19d05e47e5 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_noWindow.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test HTTP auth prompts by loading authenticate.sjs with no window</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// Let prompt_common know what kind of modal type is enabled for auth prompts. +modalType = authPromptModalType; + +add_setup(async () => { + await setStoredLoginsAsync( + ["http://mochi.test:8888", null, "mochitest", "mochiuser1", "mochipass1", "", ""] + ); +}); + +add_task(async function test_sandbox_xhr() { + const state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "mochiuser1", + passValue: "mochipass1", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + const action = { + buttonClick: "ok", + }; + const promptDone = handlePrompt(state, action); + + const url = new URL("authenticate.sjs?user=mochiuser1&pass=mochipass1", window.location.href); + const sandboxConstructor = SpecialPowers.Cu.Sandbox; + const sandbox = new sandboxConstructor(this, {wantXrays: true}); + function sandboxedRequest(sandboxedUrl) { + const req = new XMLHttpRequest(); + req.open("GET", sandboxedUrl, true); + req.send(null); + } + + const loginModifiedPromise = promiseStorageChanged(["modifyLogin"]); + sandbox.sandboxedRequest = sandboxedRequest(url); + info("send the XHR request in the sandbox"); + SpecialPowers.Cu.evalInSandbox("sandboxedRequest;", sandbox); + + await promptDone; + info("prompt shown, waiting for metadata updates"); + // Ensure the timeLastUsed and timesUsed metadata are updated. + await loginModifiedPromise; +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html new file mode 100644 index 0000000000..b82ea67faa --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth.html @@ -0,0 +1,370 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test promptAuth prompts</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content" style="display: none"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +const level = Ci.nsIAuthPrompt2.LEVEL_NONE; +const authinfo = { + username: "", + password: "", + domain: "", + + flags: Ci.nsIAuthInformation.AUTH_HOST, + authenticationScheme: "basic", + realm: "", +}; + +// Let prompt_common know what kind of modal type is enabled for auth prompts. +modalType = authPromptModalType; + +const prompterParent = runInParent(() => { + const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"]. + getService(Ci.nsIPromptFactory); + + const chromeWin = Services.wm.getMostRecentWindow("navigator:browser"); + const prompter2 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt2); + prompter2.QueryInterface(Ci.nsILoginManagerAuthPrompter).browser = chromeWin.gBrowser.selectedBrowser; + + const channels = {}; + channels.channel1 = Services.io.newChannel("http://example.com", + null, + null, + null, // aLoadingNode + Services. + scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + + channels.channel2 = Services.io.newChannel("http://example2.com", + null, + null, + null, // aLoadingNode + Services. + scriptSecurityManager.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + + addMessageListener("proxyPrompter", function onMessage(msg) { + const args = [...msg.args]; + const channelName = args.shift(); + // Replace the channel name string (arg. 0) with the channel by that name. + args.unshift(channels[channelName]); + + const rv = prompter2[msg.methodName](...args); + return { + rv, + // Send the args back to content so out/inout args can be checked. + args: msg.args, + }; + }); +}); + +const prompter2 = new PrompterProxy(prompterParent); + +add_setup(async () => { + await setStoredLoginsAsync( + ["http://example.com", null, "http://example.com", "", "examplepass", "", ""], + ["http://example2.com", null, "http://example2.com", "user1name", "user1pass", "", ""], + ["http://example2.com", null, "http://example2.com", "user2name", "user2pass", "", ""], + ["http://example2.com", null, "http://example2.com", "user3.name@host", "user3pass", "", ""], + ["http://example2.com", null, "http://example2.com", "100@beef", "user3pass", "", ""], + ["http://example2.com", null, "http://example2.com", "100%beef", "user3pass", "", ""] + ); +}); + +add_task(async function test_accept() { + const state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "inuser", + passValue: "inpass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + const action = { + buttonClick: "ok", + textField: "outuser", + passField: "outpass", + }; + authinfo.username = "inuser"; + authinfo.password = "inpass"; + authinfo.realm = "some realm"; + + promptDone = handlePrompt(state, action); + // Since prompter2 is actually a proxy to send a message to a chrome script and + // we can't send a channel in a message, we instead send the channel name that + // already exists in the chromeScript. + const isOk = prompter2.promptAuth("channel1", level, authinfo); + await promptDone; + + ok(isOk, "Checking dialog return value (accept)"); + is(authinfo.username, "outuser", "Checking returned username"); + is(authinfo.password, "outpass", "Checking returned password"); +}); + +add_task(async function test_cancel() { + const state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "outuser", + passValue: "outpass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + const action = { + buttonClick: "cancel", + }; + promptDone = handlePrompt(state, action); + const isOk = prompter2.promptAuth("channel1", level, authinfo); + await promptDone; + + ok(!isOk, "Checking dialog return value (cancel)"); +}); + +add_task(async function test_pwonly() { + // test filling in password-only login + const state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "", + passValue: "examplepass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + const action = { + buttonClick: "ok", + }; + authinfo.username = ""; + authinfo.password = ""; + authinfo.realm = "http://example.com"; + + promptDone = handlePrompt(state, action); + const isOk = prompter2.promptAuth("channel1", level, authinfo); + await promptDone; + + ok(isOk, "Checking dialog return value (accept)"); + is(authinfo.username, "", "Checking returned username"); + is(authinfo.password, "examplepass", "Checking returned password"); +}); + +add_task(async function test_multipleExisting() { + // test filling in existing login (undetermined from multiple selection) + // user2name/user2pass would also be valid to fill here. + const state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "user1name", + passValue: "user1pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + const action = { + buttonClick: "ok", + }; + authinfo.username = ""; + authinfo.password = ""; + authinfo.realm = "http://example2.com"; + + promptDone = handlePrompt(state, action); + const isOk = prompter2.promptAuth("channel2", level, authinfo); + await promptDone; + + ok(isOk, "Checking dialog return value (accept)"); + ok(authinfo.username == "user1name" || authinfo.username == "user2name", "Checking returned username"); + ok(authinfo.password == "user1pass" || authinfo.password == "user2pass", "Checking returned password"); +}); + +add_task(async function test_multipleExisting2() { + // test filling in existing login (undetermined --> user1) + // user2name/user2pass would also be valid to fill here. + const state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "user1name", + passValue: "user1pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + // enter one of the known logins, test 504+505 exercise the two possible states. + const action = { + buttonClick: "ok", + textField: "user1name", + passField: "user1pass", + }; + authinfo.username = ""; + authinfo.password = ""; + authinfo.realm = "http://example2.com"; + + promptDone = handlePrompt(state, action); + const isOk = prompter2.promptAuth("channel2", level, authinfo); + await promptDone; + + ok(isOk, "Checking dialog return value (accept)"); + is(authinfo.username, "user1name", "Checking returned username"); + is(authinfo.password, "user1pass", "Checking returned password"); +}); + +add_task(async function test_multipleExisting3() { + // test filling in existing login (undetermined --> user2) + // user2name/user2pass would also be valid to fill here. + const state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "user1name", + passValue: "user1pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + // enter one of the known logins, test 504+505 exercise the two possible states. + const action = { + buttonClick: "ok", + textField: "user2name", + passField: "user2pass", + }; + authinfo.username = ""; + authinfo.password = ""; + authinfo.realm = "http://example2.com"; + + promptDone = handlePrompt(state, action); + const isOk = prompter2.promptAuth("channel2", level, authinfo); + await promptDone; + + ok(isOk, "Checking dialog return value (accept)"); + is(authinfo.username, "user2name", "Checking returned username"); + is(authinfo.password, "user2pass", "Checking returned password"); +}); + +add_task(async function test_changingMultiple() { + // test changing a password (undetermined --> user2 w/ newpass) + // user2name/user2pass would also be valid to fill here. + const state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "user1name", + passValue: "user1pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + // force to user2, and change the password + const action = { + buttonClick: "ok", + textField: "user2name", + passField: "NEWuser2pass", + }; + authinfo.username = ""; + authinfo.password = ""; + authinfo.realm = "http://example2.com"; + + promptDone = handlePrompt(state, action); + const isOk = prompter2.promptAuth("channel2", level, authinfo); + await promptDone; + + ok(isOk, "Checking dialog return value (accept)"); + is(authinfo.username, "user2name", "Checking returned username"); + is(authinfo.password, "NEWuser2pass", "Checking returned password"); +}); + +add_task(async function test_changingMultiple2() { + // test changing a password (undetermined --> user2 w/ origpass) + // user2name/user2pass would also be valid to fill here. + const state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "user1name", + passValue: "user1pass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + // force to user2, and change the password back + const action = { + buttonClick: "ok", + textField: "user2name", + passField: "user2pass", + }; + authinfo.username = ""; + authinfo.password = ""; + authinfo.realm = "http://example2.com"; + + promptDone = handlePrompt(state, action); + const isOk = prompter2.promptAuth("channel2", level, authinfo); + await promptDone; + + ok(isOk, "Checking dialog return value (accept)"); + is(authinfo.username, "user2name", "Checking returned username"); + is(authinfo.password, "user2pass", "Checking returned password"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html new file mode 100644 index 0000000000..6f78d62f1b --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_prompt_promptAuth_proxy.html @@ -0,0 +1,269 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test promptAuth proxy prompts</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content" style="display: none"> + <iframe id="iframe"></iframe> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +/* eslint-disable mozilla/use-chromeutils-generateqi */ + +const LEVEL = Ci.nsIAuthPrompt2.LEVEL_NONE; + +let proxyAuthinfo = { + username: "", + password: "", + domain: "", + + flags: Ci.nsIAuthInformation.AUTH_PROXY, + authenticationScheme: "basic", + realm: "", +}; + +// Let prompt_common know what kind of modal type is enabled for auth prompts. +modalType = authPromptModalType; + +let chromeScript = runInParent(() => { + const promptFac = Cc[ + "@mozilla.org/passwordmanager/authpromptfactory;1" + ].getService(Ci.nsIPromptFactory); + + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + + let chromeWin = Services.wm.getMostRecentWindow("navigator:browser"); + let prompter2 = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt2); + prompter2.QueryInterface(Ci.nsILoginManagerAuthPrompter).browser = + chromeWin.gBrowser.selectedBrowser; + + let mozproxyURL; + let proxyChannel; + + addMessageListener("init", () => init()); + + addMessageListener("proxyPrompter", function onMessage(msg) { + let args = [...msg.args]; + + args[0] = proxyChannel; + let rv = prompter2[msg.methodName](...args); + return { + rv, + // Send the args back to content so out/inout args can be checked. + args: msg.args, + }; + }); + + addMessageListener("getTimeLastUsed", async () => { + let logins = await Services.logins.searchLoginsAsync({ origin: mozproxyURL, httpRealm: "Proxy Realm"}); + return logins[0].QueryInterface(Ci.nsILoginMetaInfo).timeLastUsed; + }); + + function init() { + // Need to allow for arbitrary network servers defined in PAC instead of a hardcoded moz-proxy. + let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(); + + let channel = Services.io.newChannel( + "http://example.com", + null, + null, + null, // aLoadingNode + systemPrincipal, + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + pps.asyncResolve(channel, 0, resolveCallback); + } + + class ProxyChannelListener { + onStartRequest(request) { + sendAsyncMessage("initDone"); + } + onStopRequest(request, status) {} + } + + async function initLogins(pi) { + mozproxyURL = `moz-proxy://${pi.host}:${pi.port}`; + + let proxyLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + + proxyLogin.init( + mozproxyURL, + null, + "Proxy Realm", + "proxuser", + "proxpass", + "", + "" + ); + + await Services.logins.addLoginAsync(proxyLogin); + } + + let resolveCallback = { + QueryInterface(iid) { + const interfaces = [Ci.nsIProtocolProxyCallback, Ci.nsISupports]; + + if ( + !interfaces.some(function (v) { + return iid.equals(v); + }) + ) { + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + } + return this; + }, + + async onProxyAvailable(req, uri, pi, status) { + await initLogins(pi); + + // I'm cheating a bit here... We should probably do some magic foo to get + // something implementing nsIProxiedProtocolHandler and then call + // NewProxiedChannel(), so we have something that's definately a proxied + // channel. But Mochitests use a proxy for a number of hosts, so just + // requesting a normal channel will give us a channel that's proxied. + // The proxyChannel needs to move to at least on-modify-request to + // have valid ProxyInfo, but we use OnStartRequest during startup() + // for simplicity. + proxyChannel = Services.io.newChannel( + "http://mochi.test:8888", + null, + null, + null, // aLoadingNode + systemPrincipal, + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + proxyChannel.asyncOpen(new ProxyChannelListener()); + }, + }; +}); + +let prompter2 = new PrompterProxy(chromeScript); + +add_setup(async () => { + let initComplete = new Promise((resolve) => + chromeScript.addMessageListener("initDone", resolve) + ); + chromeScript.sendAsyncMessage("init"); + info("Waiting for startup to complete..."); + await initComplete; +}); + +add_task(async function test_noAutologin() { + // test proxy login (default = no autologin), make sure it prompts. + let state = { + msg: + "The proxy moz-proxy://127.0.0.1:8888 is requesting a username and password. The site says: “Proxy Realm”", + title: "Authentication Required", + textValue: "proxuser", + passValue: "proxpass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + proxyAuthinfo.username = ""; + proxyAuthinfo.password = ""; + proxyAuthinfo.realm = "Proxy Realm"; + proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY; + + let time1 = await chromeScript.sendQuery("getTimeLastUsed"); + promptDone = handlePrompt(state, action); + let isOk = prompter2.promptAuth(null, LEVEL, proxyAuthinfo); + await promptDone; + let time2 = await chromeScript.sendQuery("getTimeLastUsed"); + + ok(isOk, "Checking dialog return value (accept)"); + isnot(time1, time2, "Checking that timeLastUsed was updated"); + is(proxyAuthinfo.username, "proxuser", "Checking returned username"); + is(proxyAuthinfo.password, "proxpass", "Checking returned password"); +}); + +add_task(async function test_autologin() { + // test proxy login (with autologin) + + // Enable the autologin pref. + await SpecialPowers.pushPrefEnv({ + set: [["signon.autologin.proxy", true]], + }); + + proxyAuthinfo.username = ""; + proxyAuthinfo.password = ""; + proxyAuthinfo.realm = "Proxy Realm"; + proxyAuthinfo.flags = Ci.nsIAuthInformation.AUTH_PROXY; + + let time1 = await chromeScript.sendQuery("getTimeLastUsed"); + let isOk = prompter2.promptAuth(null, LEVEL, proxyAuthinfo); + let time2 = await chromeScript.sendQuery("getTimeLastUsed"); + + ok(isOk, "Checking dialog return value (accept)"); + isnot(time1, time2, "Checking that timeLastUsed was updated"); + is(proxyAuthinfo.username, "proxuser", "Checking returned username"); + is(proxyAuthinfo.password, "proxpass", "Checking returned password"); +}); + +add_task(async function test_autologin_incorrect() { + // test proxy login (with autologin), ensure it prompts after a failed auth. + let state = { + msg: + "The proxy moz-proxy://127.0.0.1:8888 is requesting a username and password. The site says: “Proxy Realm”", + title: "Authentication Required", + textValue: "proxuser", + passValue: "proxpass", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + + proxyAuthinfo.username = ""; + proxyAuthinfo.password = ""; + proxyAuthinfo.realm = "Proxy Realm"; + proxyAuthinfo.flags = + Ci.nsIAuthInformation.AUTH_PROXY | Ci.nsIAuthInformation.PREVIOUS_FAILED; + + let time1 = await chromeScript.sendQuery("getTimeLastUsed"); + promptDone = handlePrompt(state, action); + let isOk = prompter2.promptAuth(null, LEVEL, proxyAuthinfo); + await promptDone; + let time2 = await chromeScript.sendQuery("getTimeLastUsed"); + + ok(isOk, "Checking dialog return value (accept)"); + isnot(time1, time2, "Checking that timeLastUsed was updated"); + is(proxyAuthinfo.username, "proxuser", "Checking returned username"); + is(proxyAuthinfo.password, "proxpass", "Checking returned password"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html new file mode 100644 index 0000000000..818a4e15bf --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_recipe_login_fields.html @@ -0,0 +1,212 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for recipes overriding login fields</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +gTestDependsOnDeprecatedLogin = true; +var chromeScript = runChecksAfterCommonInit(); + +let fillPromiseResolvers = []; + +function waitForFills(fillCount) { + let promises = []; + while (fillCount--) { + let promise = new Promise(resolve => fillPromiseResolvers.push(resolve)); + promises.push(promise); + } + + return Promise.all(promises); +} + +add_setup(async () => { + // This test should run without any existing loaded recipes interfering. + await resetRecipes(); + + if (document.readyState !== "complete") { + await new Promise((resolve) => { + document.onreadystatechange = () => { + if (document.readyState !== "complete") { + return; + } + document.onreadystatechange = null; + resolve(); + }; + }); + } + + document.getElementById("content") + .addEventListener("input", function handleInputEvent(evt) { + let resolve = fillPromiseResolvers.shift(); + if (!resolve) { + ok(false, "Too many fills"); + return; + } + + resolve(evt.target); + }); +}); + +add_task(async function loadUsernamePasswordSelectorRecipes() { + await loadRecipes({ + siteRecipes: [{ + hosts: ["mochi.test:8888"], + usernameSelector: "input[name='uname1']", + passwordSelector: "input[name='pword2']", + }], + }); +}); + +add_task(async function testOverriddingFields() { + // Insert the form dynamically so autofill is triggered after setup above. + document.getElementById("content").innerHTML = ` + <!-- form with recipe for the username and password --> + <form id="form1"> + <input type="text" name="uname1" data-expected="true"> + <input type="text" name="uname2" data-expected="false"> + <input type="password" name="pword1" data-expected="false"> + <input type="password" name="pword2" data-expected="true"> + </form>`; + + let elements = await waitForFills(2); + for (let element of elements) { + is(element.dataset.expected, "true", `${element.name} was filled`); + } +}); + +add_task(async function testDefaultHeuristics() { + // Insert the form dynamically so autofill is triggered after setup above. + document.getElementById("content").innerHTML = ` + <!-- Fallback to the default heuristics since the selectors don't match --> + <form id="form2"> + <input type="text" name="uname3" data-expected="false"> + <input type="text" name="uname4" data-expected="true"> + <input type="password" name="pword3" data-expected="true"> + <input type="password" name="pword4" data-expected="false"> + </form>`; + + let elements = await waitForFills(2); + for (let element of elements) { + is(element.dataset.expected, "true", `${element.name} was filled`); + } +}); + +add_task(async function loadNotUsernameSelectorRecipes() { + await resetRecipes(); + await loadRecipes({ + siteRecipes: [{ + hosts: ["mochi.test:8888"], + notUsernameSelector: "input[name='not_uname1']", + }], + }); +}); + +add_task(async function testNotUsernameField() { + document.getElementById("content").innerHTML = ` + <!-- The field matching notUsernameSelector should be skipped --> + <form id="form3"> + <input type="text" name="uname5" data-expected="true"> + <input type="text" name="not_uname1" data-expected="false"> + <input type="password" name="pword5" data-expected="true"> + </form>`; + + let elements = await waitForFills(2); + for (let element of elements) { + is(element.dataset.expected, "true", `${element.name} was filled`); + } +}); + +add_task(async function testNotUsernameFieldNoUsername() { + document.getElementById("content").innerHTML = ` + <!-- The field matching notUsernameSelector should be skipped. + No username field should be found and filled in this case --> + <form id="form4"> + <input type="text" name="not_uname1" data-expected="false"> + <input type="password" name="pword6" data-expected="true"> + </form>`; + + let elements = await waitForFills(1); + for (let element of elements) { + is(element.dataset.expected, "true", `${element.name} was filled`); + } +}); + +add_task(async function loadNotPasswordSelectorRecipes() { + await resetRecipes(); + await loadRecipes({ + siteRecipes: [{ + hosts: ["mochi.test:8888"], + notPasswordSelector: "input[name='not_pword'], input[name='not_pword2']", + }], + }); +}); + +add_task(async function testNotPasswordField() { + document.getElementById("content").innerHTML = ` + <!-- The field matching notPasswordSelector should be skipped --> + <form id="form5"> + <input type="text" name="uname7" data-expected="true"> + <input type="password" name="not_pword" data-expected="false"> + <input type="password" name="pword7" data-expected="true"> + </form>`; + + let elements = await waitForFills(2); + for (let element of elements) { + is(element.dataset.expected, "true", `${element.name} was filled`); + } +}); + +add_task(async function testNotPasswordFieldNoPassword() { + document.getElementById("content").innerHTML = ` + <!-- The field matching notPasswordSelector should be skipped. + No username or password field should be found and filled in this case. + A dummy form7 is added after so we know when the login manager is done + considering filling form6. --> + <form id="form6"> + <input type="text" name="uname8" data-expected="false"> + <input type="password" name="not_pword" data-expected="false"> + </form> + <form id="form7"> + <input type="password" name="pword9" data-expected="true"> + </form>`; + + let elements = await waitForFills(1); + for (let element of elements) { + is(element.dataset.expected, "true", `${element.name} was filled`); + } +}); + +add_task(async function testNotPasswordField_tooManyToOkay() { + document.getElementById("content").innerHTML = ` + <!-- The field matching notPasswordSelector should be skipped so we won't + have too many pw fields to handle (3). --> + <form id="form8"> + <input type="text" name="uname9" data-expected="true"> + <input type="password" name="not_pword2" data-expected="false"> + <input type="password" name="not_pword" data-expected="false"> + <input type="password" name="pword10" data-expected="true"> + <input type="password" name="pword11" data-expected="false"> + <input type="password" name="pword12" data-expected="false"> + </form>`; + + let elements = await waitForFills(2); + for (let element of elements) { + is(element.dataset.expected, "true", `${element.name} was filled`); + } +}); + +</script> + +<p id="display"></p> + +<div id="content"> + // Forms are inserted dynamically +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_set_stored_logins_during_task.html b/toolkit/components/passwordmgr/test/mochitest/test_set_stored_logins_during_task.html new file mode 100644 index 0000000000..36db568708 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_set_stored_logins_during_task.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Login Manager Test Helper: setLoginsDuringTest</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +Testing the test helpers `setStoredLoginsDuringTask` and `setStoredLoginsDuringTest`, which prepares the login storage during a test or task. + +<script class="testbody" type="text/javascript"> +add_setup(async () => { + await setStoredLoginsDuringTest( + ["https://example.com", "https://example.com", null, "example.com-user-one", "password", "uname", "pword"], + ["https://example.com", "https://example.com", null, "example.com-user-two", "password", "uname", "pword"], + ); +}); + +add_named_task("logins should have been stored during setup", async () => { + const logins = await getLogins(); + is(logins.length, 2, "two logins from setup should have been stored"); + is(logins[0][3], "example.com-user-one", "Login one should have been restored"); + is(logins[1][3], "example.com-user-two", "Login two should have been restored"); +}); + +add_named_task("logins should be overwritten", async () => { + await setStoredLoginsDuringTask( + ["https://example.com", "https://example.com", null, "example.com-user-three", "password", "uname", "pword"], + ); + const logins = await getLogins(); + is(logins.length, 1, "logins should have been set to single login"); + is(logins[0][3], "example.com-user-three", "Login three should have been restored"); +}); + +add_named_task("logins should get restored", async () => { + const logins = await getLogins(); + is(logins.length, 2, "Logins should have been restored"); + is(logins[0][3], "example.com-user-one", "Login one should have been restored"); + is(logins[1][3], "example.com-user-two", "Login two should have been restored"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_submit_without_field_modifications.html b/toolkit/components/passwordmgr/test/mochitest/test_submit_without_field_modifications.html new file mode 100644 index 0000000000..c31efe6a98 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_submit_without_field_modifications.html @@ -0,0 +1,311 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Don't send onFormSubmit message on navigation if the user did not interact + with the login fields</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <iframe id="loginFrame"> + </iframe> +</div> + +<pre id="test"></pre> +<script> +SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popup to show"); + +const EXAMPLE_COM = window.location.origin + "/tests/toolkit/components/passwordmgr/test/mochitest/"; +const PREFILLED_FORM_URL = EXAMPLE_COM + "subtst_prefilled_form.html" + +let iframe = document.getElementById("loginFrame"); + +function waitForLoad() { + return new Promise(resolve => { + function handleLoad() { + iframe.removeEventListener("load", handleLoad); + resolve(); + } + iframe.addEventListener("load", handleLoad); + }); +} + +async function setupWithOneLogin(pageUrl) { + let origin = window.location.origin; + await setStoredLoginsAsync([origin, origin, null, "user1", "pass1"]); + + let chromeScript = runInParent(async function testSetup() { + let logins = await Services.logins.getAllLogins(); + for (let l of logins) { + info("Got login: " + l.username + ", " + l.password); + } + }); + + await setup(pageUrl); + return chromeScript; +} + +function resetSavedLogins() { + let chromeScript = runInParent(function testTeardown() { + Services.logins.removeAllUserFacingLogins(); + }); + chromeScript.destroy(); +} + +async function setup(pageUrl) { + let loadPromise = waitForLoad(); + let processedFormPromise = promiseFormsProcessed(); + iframe.src = pageUrl; + + await processedFormPromise; + info("initial form processed"); + await loadPromise; + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + let doc = this.content.document; + let link = doc.createElement("a"); + link.setAttribute("href", "http://mochi.test:8888"); + doc.body.appendChild(link); + }); +} + +async function navigateWithoutUserInteraction() { + let loadPromise = waitForLoad(); + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + let doc = this.content.document; + let hadInteracted = doc.userHasInteracted; + let target = doc.querySelector("a[href]"); + if (target) { + target.click(); + } else { + target = doc.querySelector("form"); + target.submit(); + } + is(doc.userHasInteracted, hadInteracted, "document.userHasInteracted shouldn't have changed"); + }); + await loadPromise; +} + +async function userInput(selector, value) { + await SpecialPowers.spawn(getIframeBrowsingContext(window), [selector, value], async function(sel, val) { + // use "real" synthesized events rather than setUserInput to ensure + // document.userHasInteracted is flipped true + let EventUtils = ContentTaskUtils.getEventUtils(content); + let target = this.content.document.querySelector(sel); + target.focus(); + target.select(); + await EventUtils.synthesizeKey("KEY_Backspace", {}, this.content); + await EventUtils.sendString(val, this.content); + info( + `userInput: new target.value: ${target.value}` + ); + target.blur(); + return Promise.resolve(); + }); +} + +function checkDocumentUserHasInteracted() { + return SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + return this.content.document.userHasInteracted; + }); +} + +add_task(async function test_init() { + // For this test, we'll be testing with & without user document interaction. + // So we'll reset the pref which dictates the behavior of + // LoginFormState._formHasModifiedFields in automation + // and ensure all interactions are properly emulated + ok(SpecialPowers.getBoolPref("signon.testOnlyUserHasInteractedByPrefValue"), "signon.testOnlyUserHasInteractedByPrefValue should default to true"); + info("test_init, flipping the signon.testOnlyUserHasInteractedByPrefValue pref"); + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.testOnlyUserHasInteractedByPrefValue", false], + ]}); + SimpleTest.registerCleanupFunction(async function cleanup_pref() { + await SpecialPowers.popPrefEnv(); + }); + + await SimpleTest.promiseWaitForCondition(() => LoginHelper.testOnlyUserHasInteractedWithDocument === null); + is(LoginHelper.testOnlyUserHasInteractedWithDocument, null, + "LoginHelper.testOnlyUserHasInteractedWithDocument should be null for this set of tests"); +}); + +add_task(async function test_no_message_on_navigation() { + // If login field values were set by the website, we don't message to save the + // login values if the user did not interact with the fields before submiting. + await setup(PREFILLED_FORM_URL); + + let submitMessageSent = false; + getSubmitMessage().then(value => { + submitMessageSent = true; + }); + await navigateWithoutUserInteraction(); + + // allow time to pass before concluding no onFormSubmit message was sent + await new Promise(res => setTimeout(res, 1000)); + ok(!submitMessageSent, "onFormSubmit message is not sent on navigation since the login fields were not modified"); +}); + +add_task(async function test_prefd_off_message_on_navigation() { + // Confirm the pref controls capture behavior with non-user-set field values. + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.userInputRequiredToCapture.enabled", false], + ]}); + await setup(PREFILLED_FORM_URL); + + let promiseSubmitMessage = getSubmitMessage(); + await navigateWithoutUserInteraction(); + await promiseSubmitMessage; + info("onFormSubmit message was sent as expected after navigation"); + + SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_message_with_user_interaction_on_navigation() { + await setup(PREFILLED_FORM_URL); + await userInput("#form-basic-username", "foo"); + + let promiseSubmitMessage = getSubmitMessage(); + await navigateWithoutUserInteraction(); + await promiseSubmitMessage; + info("onFormSubmit message was sent as expected after user interaction"); +}); + +add_task(async function test_empty_form_with_input_handler() { + await setup(EXAMPLE_COM + "formless_basic.html"); + await userInput("#form-basic-username", "user"); + await userInput("#form-basic-password", "pass"); + + let promiseSubmitMessage = getSubmitMessage(); + await navigateWithoutUserInteraction(); + await promiseSubmitMessage; + info("onFormSubmit message was sent as expected after user interaction"); +}); + +add_task(async function test_no_message_on_autofill_without_user_interaction() { + let chromeScript = await setupWithOneLogin(EXAMPLE_COM + "form_basic.html"); + // Check for autofilled values. + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), + "form-basic-username", "user1", + "form-basic-password", "pass1"); + + info("LoginHelper.testOnlyUserHasInteractedWithDocument:" + + LoginHelper.testOnlyUserHasInteractedWithDocument + ); + ok(!(await checkDocumentUserHasInteracted()), "document.userHasInteracted should be initially false"); + let submitMessageSent = false; + getSubmitMessage().then(value => { + submitMessageSent = true; + }); + info("Navigating the page") + await navigateWithoutUserInteraction(); + + // allow time to pass before concluding no onFormSubmit message was sent + await new Promise(res => setTimeout(res, 1000)); + + chromeScript.destroy(); + resetSavedLogins(); + + ok(!submitMessageSent, "onFormSubmit message is not sent on navigation since the document had no user interaction"); +}); + +add_task(async function test_message_on_autofill_with_document_interaction() { + // We expect that as long as the form values !== their defaultValues, + // any document interaction allows the submit message to be sent + + let chromeScript = await setupWithOneLogin(EXAMPLE_COM + "form_basic.html"); + // Check for autofilled values. + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), + "form-basic-username", "user1", + "form-basic-password", "pass1"); + + let userInteracted = await checkDocumentUserHasInteracted(); + ok(!userInteracted, "document.userHasInteracted should be initially false"); + + await SpecialPowers.spawn(getIframeBrowsingContext(window), ["#form-basic-username"], async function(sel) { + // Click somewhere in the document to ensure document.userHasInteracted is flipped to true + let EventUtils = ContentTaskUtils.getEventUtils(content); + let target = this.content.document.querySelector(sel); + + await EventUtils.synthesizeMouseAtCenter(target, {}, this.content); + }); + + userInteracted = await checkDocumentUserHasInteracted(); + ok(userInteracted, "After synthesizeMouseAtCenter, document.userHasInteracted should be true"); + + let promiseSubmitMessage = getSubmitMessage(); + await navigateWithoutUserInteraction(); + + let { data } = await promiseSubmitMessage; + ok(data.autoFilledLoginGuid, "Message was sent with autoFilledLoginGuid"); + info("Message was sent as expected after document user interaction"); + + chromeScript.destroy(); + resetSavedLogins(); +}); + +add_task(async function test_message_on_autofill_with_user_interaction() { + // Editing a field value causes the submit message to be sent as + // there is both document interaction and field modification + let chromeScript = await setupWithOneLogin(EXAMPLE_COM + "form_basic.html"); + // Check for autofilled values. + await checkLoginFormInFrame(getIframeBrowsingContext(window, 0), + "form-basic-username", "user1", + "form-basic-password", "pass1"); + + userInput("#form-basic-username", "newuser"); + let promiseSubmitMessage = getSubmitMessage(); + await navigateWithoutUserInteraction(); + + let { data } = await promiseSubmitMessage; + ok(data.autoFilledLoginGuid, "Message was sent with autoFilledLoginGuid"); + is(data.usernameField.value, "newuser", "Message was sent with correct usernameField.value"); + info("Message was sent as expected after user form interaction"); + + chromeScript.destroy(); + resetSavedLogins(); +}); + +add_task(async function test_no_message_on_user_input_from_other_form() { + // ensure input into unrelated fields on the page don't change login form modified-ness + await setup(PREFILLED_FORM_URL); + + // Add a form which will not be submitted and an input associated with that form + await SpecialPowers.spawn(getIframeBrowsingContext(window), [], function() { + let doc = this.content.document; + let loginForm = doc.querySelector("form"); + let fragment = doc.createDocumentFragment(); + let otherForm = doc.createElement("form"); + otherForm.id ="otherForm"; + fragment.appendChild(otherForm); + + let alienField = doc.createElement("input"); + alienField.id = "alienField"; + alienField.type = "text"; // not a password field + alienField.setAttribute("form", "otherForm"); + // new field is child of the login, but a member of different non-login form via its .form property + loginForm.appendChild(alienField); + doc.body.appendChild(fragment); + }); + await userInput("#alienField", "something"); + + let submitMessageSent = false; + getSubmitMessage().then(data => { + info("submit mesage data: " + JSON.stringify(data)); + submitMessageSent = true; + }); + + info("submitting the form"); + await navigateWithoutUserInteraction(); + + // allow time to pass before concluding no onFormSubmit message was sent + await new Promise(res => setTimeout(res, 1000)); + ok(!submitMessageSent, "onFormSubmit message is not sent on navigation since no login fields were modified"); +}); + +</script> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html new file mode 100644 index 0000000000..9c2f9d8e89 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html @@ -0,0 +1,166 @@ + +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test interaction between autocomplete and focus on username fields</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="../../../satchel/test/satchel_common.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +const action1 = "http://username-focus-1"; +const action2 = "http://username-focus-2"; + +add_setup(async () => { + await setStoredLoginsAsync( + [location.origin, action1, null, "testuser1A", "testpass1A", "", ""], + [location.origin, action2, null, "testuser2A", "testpass2A", "", ""], + [location.origin, action2, null, "testuser2B", "testpass2B", "", ""] + ); +}); + +add_task(async function autofilled() { + const form = createLoginForm({ action: action1 }); + + info("Username and password already filled so don't show autocomplete"); + await noPopupBy(() => form.uname.focus()); + + form.submit.focus(); + form.uname.value = "testuser"; + info("Focus when we don't have an exact match"); + await popupBy(() => form.uname.focus()); +}); + +add_task(async function autofilled_prefilled_un() { + const form = createLoginForm({ + action: action1, + username: { + value: "testuser1A" + } + }); + + info("Username and password already filled so don't show autocomplete"); + await noPopupBy(() => form.uname.focus()); + + form.submit.focus(); + form.uname.value = "testuser"; + info("Focus when we don't have an exact match"); + await popupBy(() => form.uname.focus()); +}); + +add_task(async function autofilled_focused_dynamic() { + const form = createLoginForm({ + action: action1, + password: { + type: "not-yet-password" + } + }); + + info("Username and password will be filled while username focused"); + await noPopupBy(() => form.uname.focus()); + + info("triggering autofill"); + await noPopupBy(() => form.pword.type = "password"); + + const popupState = await getPopupState(); + is(popupState.open, false, "Check popup is closed"); + + form.submit.focus(); + form.pword.value = "test"; + info("Focus when we don't have an exact match"); + await popupBy(() => form.uname.focus()); +}); + +// Begin testing forms that have multiple saved logins + +add_task(async function multiple() { + const form = createLoginForm({ action: action2 }); + + info("Fields not filled due to multiple so autocomplete upon focus"); + await popupBy(() => form.uname.focus()); +}); + +add_task(async function multiple_dynamic() { + const form = createLoginForm({ + action: action2, + password: { + type: "not-yet-password" + } + }); + + info("Fields not filled but username is focused upon marking so open"); + await noPopupBy(() => form.uname.focus()); + + info("triggering _fillForm code"); + await popupBy(() => form.pword.type = "password"); +}); + +add_task(async function multiple_prefilled_un1() { + const form = createLoginForm({ + action: action2, + username: { + value: "testuser2A" + } + }); + + info("Username and password already filled so don't show autocomplete"); + await noPopupBy(() => form.uname.focus()); + + form.submit.focus(); + form.uname.value = "testuser"; + info("Focus when we don't have an exact match"); + await popupBy(() => form.uname.focus()); +}); + +add_task(async function multiple_prefilled_un2() { + const form = createLoginForm({ + action: action2, + username: { + value: "testuser2B" + } + }); + + info("Username and password already filled so don't show autocomplete"); + await noPopupBy(() => form.uname.focus()); + + form.submit.focus(); + form.uname.value = "testuser"; + info("Focus when we don't have an exact match"); + await popupBy(() => form.uname.focus()); +}); + +add_task(async function multiple_prefilled_focused_dynamic() { + const form = createLoginForm({ + action: action2, + username: { + value: "testuser2B" + }, + password: { + type: "not-yet-password" + } + }); + + info("Username and password will be filled while username focused"); + await noPopupBy(() => form.uname.focus()); + info("triggering autofill"); + await noPopupBy(() => form.pword.type = "password"); + + let popupState = await getPopupState(); + is(popupState.open, false, "Check popup is closed"); + + form.submit.focus(); + form.pword.value = "test"; + info("Focus when we don't have an exact match"); + await popupBy(() => form.uname.focus()); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_xhr.html b/toolkit/components/passwordmgr/test/mochitest/test_xhr.html new file mode 100644 index 0000000000..811a2e7759 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_xhr.html @@ -0,0 +1,164 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for XHR prompts</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <script type="text/javascript" src="../../../prompts/test/prompt_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Login Manager test: XHR prompt +<p id="display"></p> + +<div id="content" style="display: none"> + <iframe id="iframe"></iframe> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: XHR prompts. **/ +function makeRequest(uri) { + return new Promise((resolve, reject) => { + let request = new XMLHttpRequest(); + request.open("GET", uri, true); + request.addEventListener("loadend", function onLoadEnd() { + let result = xhrLoad(request.responseXML); + resolve(result); + }); + request.send(null); + }); +} + +function xhrLoad(xmlDoc) { + // The server echos back the user/pass it received. + var username = xmlDoc.getElementById("user").textContent; + var password = xmlDoc.getElementById("pass").textContent; + var authok = xmlDoc.getElementById("ok").textContent; + return {username, password, authok}; +} + +// Let prompt_common know what kind of modal type is enabled for auth prompts. +modalType = authPromptModalType; + +let prompterParent = runInParent(() => { + const promptFac = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"]. + getService(Ci.nsIPromptFactory); + + let chromeWin = Services.wm.getMostRecentWindow("navigator:browser"); + let prompt = promptFac.getPrompt(chromeWin, Ci.nsIAuthPrompt); + + addMessageListener("proxyPrompter", function onMessage(msg) { + let rv = prompt[msg.methodName](...msg.args); + return { + rv, + // Send the args back to content so out/inout args can be checked. + args: msg.args, + }; + }); +}); + +let prompter1 = new PrompterProxy(prompterParent); + +add_setup(async () => { + await setStoredLoginsAsync( + ["http://mochi.test:8888", null, "xhr", "xhruser1", "xhrpass1"], + ["http://mochi.test:8888", null, "xhr2", "xhruser2", "xhrpass2"] + ); +}); + +add_task(async function test1() { + let state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "xhruser1", + passValue: "xhrpass1", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + let action = { + buttonClick: "ok", + }; + let promptDone = handlePrompt(state, action); + let requestPromise = makeRequest("authenticate.sjs?user=xhruser1&pass=xhrpass1&realm=xhr"); + await promptDone; + let result = await requestPromise; + + is(result.authok, "PASS", "Checking for successful authentication"); + is(result.username, "xhruser1", "Checking for username"); + is(result.password, "xhrpass1", "Checking for password"); +}); + +add_task(async function test2() { + // Test correct parenting, by opening another tab in the foreground, + // and making sure the prompt re-focuses the original tab when shown: + let newWin = window.open(); + newWin.focus(); + + let state = { + msg: "This site is asking you to sign in.", + title: "Authentication Required", + textValue: "xhruser2", + passValue: "xhrpass2", + iconClass: "authentication-icon question-icon", + titleHidden: true, + textHidden: false, + passHidden: false, + checkHidden: true, + checkMsg: "", + checked: false, + focused: "textField", + defButton: "button0", + }; + + // For window prompts check that the dialog is modal, chrome and dependent; + // We can't just check window.opener because that'll be + // a content window, which therefore isn't exposed (it'll lie and + // be null). + if (authPromptModalType === SpecialPowers.Services.prompt.MODAL_TYPE_WINDOW) { + state.chrome = true; + state.dialog = true; + state.chromeDependent = true; + state.isWindowModal = true; + } + + let action = { + buttonClick: "ok", + }; + let promptDone = handlePrompt(state, action); + let requestPromise = makeRequest("authenticate.sjs?user=xhruser2&pass=xhrpass2&realm=xhr2"); + await promptDone; + let result = await requestPromise; + + runInParent(() => { + // Check that the right tab is focused: + let browserWin = Services.wm.getMostRecentWindow("navigator:browser"); + let spec = browserWin.gBrowser.selectedBrowser.currentURI.spec; + assert.ok(spec.startsWith(window.location.origin), + `Tab with remote URI (rather than about:blank) + should be focused (${spec})`); + }); + + is(result.authok, "PASS", "Checking for successful authentication"); + is(result.username, "xhruser2", "Checking for username"); + is(result.password, "xhrpass2", "Checking for password"); + + // Wait for the assert from the parent script to run and send back its reply, + // so it's processed before the test ends. + await SpecialPowers.executeAfterFlushingMessageQueue(); + + newWin.close(); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html new file mode 100644 index 0000000000..16d5e786e0 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_xhr_2.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=654348 +--> +<head> + <meta charset="utf-8"> + <title>Test XHR auth with user and pass arguments</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="pwmgr_common.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="startTest()"> +<script class="testbody" type="text/javascript"> + +/** + * This test checks we correctly ignore authentication entry + * for a subpath and use creds from the URL when provided when XHR + * is used with filled user name and password. + * + * 1. connect authenticate.sjs that excepts user1:pass1 password + * 2. connect authenticate.sjs that this time expects differentuser2:pass2 password + * we must use the creds that are provided to the xhr witch are different and expected + */ + +function doxhr(URL, user, pass, code, next) { + var xhr = new XMLHttpRequest(); + if (user && pass) { + xhr.open("POST", URL, true, user, pass); + } else { + xhr.open("POST", URL, true); + } + xhr.onload = function() { + is(xhr.status, code, "expected response code " + code); + next(); + }; + xhr.onerror = function() { + ok(false, "request passed"); + finishTest(); + }; + xhr.send(); +} + +function startTest() { + doxhr("authenticate.sjs?user=dummy&pass=pass1&realm=realm1&formauth=1", "dummy", "dummy", 403, function() { + doxhr("authenticate.sjs?user=dummy&pass=pass1&realm=realm1&formauth=1", "dummy", "pass1", 200, finishTest); + }); +} + +function finishTest() { + SimpleTest.finish(); +} + +</script> +</body> +</html> |