diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation_confirm.html | 518 |
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> |