summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html518
1 files changed, 518 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html
new file mode 100644
index 0000000000..cd5d5952f6
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html
@@ -0,0 +1,518 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test filling generated passwords into confirm password fields</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script src="pwmgr_common.js"></script>
+ <script src="../../../satchel/test/satchel_common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+Login Manager test: filling generated passwords into confirm password fields
+
+<p id="display"></p>
+
+<!-- we presumably can't hide the content for this test. -->
+<div id="content">
+
+ <!-- form1 has new-password followed by confirm-password fields -->
+ <form id="form1" action="https://example.com" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="new-password">
+ <input type="password" name="pword-next">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form2 has 2 new-password fields -->
+ <form id="form2" action="https://example.com" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="new-password">
+ <input type="password" name="pword-between">
+ <input type="password" name="pword-next" autocomplete="new-password">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form3 has lots of junk fields before the confirm-password field -->
+ <form id="form3" action="https://example.com" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="new-password">
+ <input type="text" name="junk0">
+ <input type="text" name="junk1">
+ <input type="text" name="junk2">
+ <input type="text" name="junk3">
+ <input type="text" name="junk4">
+ <input type="password" name="pword-next">
+ <button type="submit">Submit</button>
+ </form>
+
+ <!-- form4 has a password field after the confirm-password field we don't want to fill into -->
+ <form id="form4" action="https://example.com" onsubmit="return false;">
+ <input type="text" name="uname">
+ <input type="password" name="pword" autocomplete="new-password">
+ <input type="password" name="pword-next" autocomplete="new-password">
+ <input type="password" name="pword-extra" autocomplete="new-password">
+ <button type="submit">Submit</button>
+ </form>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const { ContentTaskUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/ContentTaskUtils.jsm"
+);
+const { TestUtils } = SpecialPowers.ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+const setupScript = runInParent(function parentTestSetup() {
+ const { LoginTestUtils } = ChromeUtils.import(
+ "resource://testing-common/LoginTestUtils.jsm"
+ );
+
+ addMessageListener(
+ "resetLoginsAndGeneratedPasswords", () => {
+ LoginTestUtils.clearData();
+ LoginTestUtils.resetGeneratedPasswordsCache();
+ }
+ );
+});
+
+function testReset() {
+ return setupScript.sendAsyncMessage("resetLoginsAndGeneratedPasswords");
+}
+
+function generateDateString(date) {
+ let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined,
+ { dateStyle: "medium" });
+ return dateAndTimeFormatter.format(date);
+}
+
+const DATE_NOW_STRING = generateDateString(new Date());
+
+async function promiseACPopupClosed() {
+ return SimpleTest.promiseWaitForCondition(async () => {
+ // FIXME(bug 1721900): previously `promiseWaitForCondition` wouldn't await
+ // on async functions, so the condition would always immediately resolve as
+ // `true`.
+ return true;
+ // let popupState = await getPopupState();
+ // return !popupState.open;
+ }, "Wait for AC popup to be closed");
+}
+
+async function fillWithGeneratedPassword(input, expectedResults = ["Use a Securely Generated Password"]) {
+ info("Opening the AC menu");
+ const { items } = await openPopupOn(input);
+ checkAutoCompleteResults(items, expectedResults,
+ window.location.host, "Check all rows are correct");
+
+ for (let autocompleteItem of items) {
+ synthesizeKey("KEY_ArrowDown");
+ if (autocompleteItem == "Use a Securely Generated Password") {
+ synthesizeKey("KEY_Enter");
+ break;
+ }
+ }
+ await TestUtils.waitForTick();
+}
+
+async function testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ passwordInputName = "pword",
+ confirmInputName = "pword-next",
+ expectedACResults,
+ expectedFilled = [], // the names of the inputs which should be filled
+ // field-name: expected-value for any non-filled inputs which should have values other than their .defaultValue
+ expectedNonDefaultValues = {},
+ expectedMasked = [], // the names of the inputs which should be masked
+ beforeFn,
+ afterFn
+}) {
+ // form should not be initially filled
+ checkForm(formNumber, "", "");
+
+ if (beforeFn) {
+ await beforeFn(document.getElementById(`form${formNumber}`));
+ document.documentElement.scrollTop; // Flush pending reflows which may be caused by beforeFn.
+ }
+ let pword = getFormElementByName(formNumber, passwordInputName || "pword");
+ let pword2 = getFormElementByName(formNumber, confirmInputName || "pword-next");
+
+ await fillWithGeneratedPassword(pword, expectedACResults);
+
+ info("waiting for the password field to be filled with the generated password");
+ await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");
+ await TestUtils.waitForTick();
+
+ let generatedPW = pword.value;
+ is(generatedPW.length, GENERATED_PASSWORD_LENGTH, "Check generated password length");
+ ok(generatedPW.match(GENERATED_PASSWORD_REGEX), "Check generated password format");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword, false, "After fill");
+
+ info("Check the expected password fields are filled");
+ for (let input of pword.form.querySelectorAll("input")) {
+ // check field filling & highlights
+ if (expectedFilled.includes(input.name)) {
+ await ContentTaskUtils.waitForCondition(() => {
+ return input.matches(":autofill");
+ }, `Highlight was successfully applied to the (${input.name}) field`);
+
+ is(input.value, generatedPW, `Field (${input.name}) has the generated password value`);
+ } else {
+ await ContentTaskUtils.waitForCondition(() => {
+ return !input.matches(":autofill");
+ }, `Highlight was not applied to field (${input.name})`);
+
+ let expectedValue = (input.name in expectedNonDefaultValues) ? expectedNonDefaultValues[input.name] : input.defaultValue;
+ is(input.value, expectedValue, `Field (${input.name}) field has the expected value`);
+ }
+
+ if (expectedMasked.includes(input.name)) {
+ LOGIN_FIELD_UTILS.checkPasswordMasked(input, true, `Field (${input.name}) should be masked`);
+ }
+ }
+
+ if (expectedFilled.includes(pword2.name)) {
+ info("Check the 2 field values aren't mirrored");
+ // changing the password field value should result in a message sent to the parent process
+ let messageSentPromise = getPasswordEditedMessage();
+
+ pword.focus();
+ // add a character. We don't expect the confirm password field to get the same change
+ synthesizeKey("KEY_End");
+ synthesizeKey("@");
+ await TestUtils.waitForTick();
+ is(pword.value.length, GENERATED_PASSWORD_LENGTH + 1, "Value of the first password field changed");
+ is(pword2.value, generatedPW, "Value of the confirm field did not change");
+
+ // focusing the 2nd password field blurs the first and results in a "change" event
+ pword2.focus();
+ info("Waiting for edit message");
+ await messageSentPromise;
+
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Generated password is masked when blurred");
+ if (expectedMasked.includes(pword2.name)) {
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword2, false, "Confirm password should be be unmasked");
+ }
+
+ // remove a character from the confirm field.
+ // We don't expect the filled password field to get the same change
+ synthesizeKey("KEY_End");
+ synthesizeKey("KEY_Backspace");
+ await TestUtils.waitForTick();
+ is(pword2.value, generatedPW.substring(0, GENERATED_PASSWORD_LENGTH - 1), "Value of the confirm field changed");
+ is(pword.value.length, GENERATED_PASSWORD_LENGTH + 1, "Value of the first password field didn't change");
+
+ // the confirm field has its highlight cleared when emptied
+ pword2.focus();
+ pword2.select();
+ synthesizeKey("KEY_Backspace");
+ await TestUtils.waitForTick();
+ await ContentTaskUtils.waitForCondition(() => {
+ return !pword2.matches(":autofill");
+ }, "Highlight was successfully cleared from the confirm-password field");
+
+ // if it got originally masked (i.e. was a password field) verify the focused confirm field now masks
+ // its input like a normal, non-generated password field after being emptied
+ if (expectedMasked.includes(pword2.name)) {
+ pword2.focus();
+ synthesizeKey("a");
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword2, true, "Confirm password gets masked again when focused");
+ }
+ } else {
+ let expectedValue = (pword2.name in expectedNonDefaultValues) ? expectedNonDefaultValues[pword2.name] : pword2.defaultValue;
+ is(pword2.value, expectedValue, "Value of the confirm-password field is unchanged");
+
+ if (pword2.type == "password" && !pword2.disabled && !pword2.readOnly) {
+ // make sure we didn't change the behavior of a non-generated password field
+ // it should remain masked when it has focus
+ pword2.focus();
+ LOGIN_FIELD_UTILS.checkPasswordMasked(pword, true, "Non-generataed password field remains masked when focused");
+ }
+ }
+
+ if (afterFn) {
+ await afterFn(document.getElementById(`form${formNumber}`));
+ document.documentElement.scrollTop; // Flush pending reflows which may be caused by afterFn.
+ }
+
+ // close any open menu
+ synthesizeKey("KEY_Escape");
+ await promiseACPopupClosed();
+
+ recreateTree(document.getElementById(`form${formNumber}`));
+}
+
+add_task(async function test_fillNextPasswordField() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ // the next password field should be filled with the generated password
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // the confirm field should be masked, the filled field has focus and should not be masked
+ expectedMasked: ["pword-next"],
+ });
+});
+
+add_task(async function test_fillNextPasswordFieldWasPasswordType() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ is(pword2.type, "password", "confirm field is of password type");
+ pword2.type = "text";
+ },
+ // the next hasBeenTypePassword field should be filled with the generated password
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // the confirm field should is currently type=text so will not be masked
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.type = "password";
+ }
+ });
+});
+
+add_task(async function test_dontFillNonEmptyPasswordField() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.setAttribute("value", "previous value");
+ },
+ // the would-be confirm field is not empty so will not be filled
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.setAttribute("value", "");
+ }
+ });
+});
+
+add_task(async function test_dontFillEditedNewPasswordField() {
+ const formNumber = 2;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.focus()
+ sendString("edited value");
+ },
+ // the would-be confirm field is not empty so will not be filled
+ expectedFilled: ["pword"],
+ expectedNonDefaultValues: {
+ "pword-next": "edited value",
+ },
+ // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.setAttribute("value", "");
+ }
+ });
+});
+
+add_task(async function test_ignoreReadOnlyField() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.readOnly = true;
+ },
+ // the confirm field candidate is read-only so will not be filled
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.readOnly = false;
+ }
+ });
+});
+
+add_task(async function test_ignoreDisabledField() {
+ const formNumber = 1;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.disabled = true;
+ },
+ // the confirm field candidate is read-only so will not be filled
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ // pword is filled by the test, but has focus so isn't masked. Its masking behavior is tested elsewhere.
+ expectedMasked: [],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.disabled = false;
+ }
+ });
+});
+
+add_task(async function test_preferMatchingAutoCompleteInfoPasswordField() {
+ const formNumber = 2;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: ["pword-next"],
+ });
+});
+
+add_task(async function test_ignoreDisabledMatchingAutoCompleteInfoPasswordField() {
+ const formNumber = 2;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.disabled = true;
+ },
+ // it should fill the next password field, not the disabled one
+ confirmInputName: "pword-between",
+ expectedFilled: ["pword", "pword-between"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: ["pword-between"],
+ afterFn(form) {
+ let pword2 = form.querySelector("input[name='pword-next']");
+ pword2.disabled = false;
+ },
+ });
+});
+
+add_task(async function test_ignoreTooDistantPasswordField() {
+ const formNumber = 3;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: [],
+ });
+});
+
+add_task(async function test_tooManyDisabledFields() {
+ // we don't fill into disabled fields,
+ // but they do count towards the distance from the first password field
+ const formNumber = 3;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ for(let inp of form.querySelectorAll("input[name*='junk']")) {
+ inp.disabled = true;
+ }
+ },
+ expectedFilled: ["pword"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: [],
+ afterFn(form) {
+ for(let inp of form.querySelectorAll("input[name*='junk']")) {
+ inp.disabled = false;
+ }
+ },
+ });
+});
+
+add_task(async function test_skipOverHiddenFields() {
+ // hidden fields do not count towards the distance from the first password field
+ const formNumber = 3;
+ await testReset();
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ beforeFn(form) {
+ for(let inp of form.querySelectorAll("input[name*='junk']")) {
+ inp.type = "hidden";
+ }
+ },
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: ["pword-next"],
+ afterFn(form) {
+ for(let inp of form.querySelectorAll("input[name*='junk']")) {
+ inp.type = "text";
+ }
+ },
+ });
+});
+
+add_task(async function test_dontFill3rdPasswordField() {
+ // if a generated password field was previously filled on a form
+ // don't look for a confirm-password field when filling another field with a generated password
+ const formNumber = 4;
+ await testReset();
+
+ let pword = getFormElementByName(formNumber, "pword");
+ let pword2 = getFormElementByName(formNumber, "pword-next");
+ let pword3 = getFormElementByName(formNumber, "pword-extra");
+ await testConfirmPasswordFieldFilledWithGeneratedPassword({
+ formNumber,
+ async beforeFn(form) {
+ // disable the following password fields so they dont get filled just yet
+ pword2.disabled = true;
+ pword3.disabled = true;
+
+ info("beforeFn, filling the pword field");
+ await fillWithGeneratedPassword(pword);
+ info("beforeFn: waiting for the password field to be filled with the generated password");
+ await SimpleTest.promiseWaitForCondition(() => !!pword.value, "Check generated pw filled");
+
+ pword2.disabled = false;
+ pword3.disabled = false;
+ is(pword2.value, "", "The pword-next field was not filled");
+ is(pword3.value, "", "The pword-extra field was not filled");
+ },
+ // Fill into the confirm-password field,
+ // we want to confirm this doesnt now try and fill another nearby "confirm" field
+ passwordInputName: "pword-next",
+ confirmInputName: "pword-extra",
+ // we already generated a password in this test, so the AC menu will have the auto-saved login
+ expectedACResults: [
+ "No username (" + DATE_NOW_STRING + ")",
+ "Use a Securely Generated Password",
+ ],
+ // we've manually filled both fields,
+ // we don't expect the third 'pword-extr' field to be filled
+ expectedFilled: ["pword", "pword-next"],
+ // all fields should either be filled or have their .defaultValue
+ expectedNonDefaultValues: {},
+ expectedMasked: [
+ "pword", // pword was filled in the beforeFn and is blurred so it should be masked
+ // pword-next was filled by the test and is focused so should not be masked
+ ],
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>