diff options
Diffstat (limited to 'toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html')
-rw-r--r-- | toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html | 574 |
1 files changed, 574 insertions, 0 deletions
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..3555b9173d --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html @@ -0,0 +1,574 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test 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> +Login Manager test: autofill with autocomplete=new-password fields + +<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 { ContentTaskUtils } = SpecialPowers.ChromeUtils.import( + "resource://testing-common/ContentTaskUtils.jsm" +); +const { TestUtils } = SpecialPowers.ChromeUtils.import( + "resource://testing-common/TestUtils.jsm" +); + +let 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 promiseACPopupClosed() { + return SimpleTest.promiseWaitForCondition(async () => { + let popupState = await getPopupState(); + return !popupState.open; + }, "Wait for AC popup to be closed"); +} + +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.import("resource://gre/modules/LoginManagerParent.jsm"); + 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"); + + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.generation.available", true], + ["signon.generation.enabled", true], + ]}); +}); + +add_task(async function test_autofillAutocompleteUsername_noGeneration() { + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.generation.available", false], + ["signon.generation.enabled", false], + ]}); + await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]); + + createLoginForm({ + num: 1, + action: "https://autofill", + password: { + name: "p" + } + }); + const form2 = createLoginForm({ + num: 2, + action: "https://autofill", + password: { + name: "password", + autocomplete: "new-password" + } + }); + await promiseFormsProcessedInSameProcess(2); + + // reference form was filled as expected? + checkForm(1, "user1", "pass1"); + + // 2nd form should not be filled + checkForm(2, "", ""); + + form2.uname.focus(); + await showACPopup(2, ["user1"]); + + let acEvents = await waitForTelemetryEventsCondition(events => { + return events.length == 1; + }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`); + checkACTelemetryEvent(acEvents[0], form2.uname, { + "hadPrevious": "0", + "login": "1", + "loginsFooter": "1" + }); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + + await promiseFormsProcessedInSameProcess(); + checkForm(2, "user1", "pass1"); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_autofillAutocompletePassword_noGeneration() { + await SpecialPowers.pushPrefEnv({"set": [ + ["signon.generation.available", false], + ["signon.generation.enabled", false], + ]}); + await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = createLoginForm({ + num: 2, + action: "https://autofill", + password: { + name: "password", + autocomplete: "new-password" + } + }); + await promiseFormsProcessedInSameProcess(); + + // 2nd form should not be filled + checkForm(2, "", ""); + + form.password.focus(); + await showACPopup(2, ["user1"]); + let acEvents = await waitForTelemetryEventsCondition(events => { + return events.length == 1; + }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`); + checkACTelemetryEvent(acEvents[0], form.password, { + "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"); + checkForm(2, "", "pass1"); + + // No autocomplete results should appear for non-empty pw fields. + await noPopupByArrowDown(); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_autofillAutocompleteUsername_noGeneration2() { + await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = createLoginForm({ + num: 2, + action: "https://autofill", + password: { + name: "password", + autocomplete: "new-password" + } + }); + await promiseFormsProcessedInSameProcess(); + + // 2nd form should not be filled + checkForm(2, "", ""); + + form.uname.focus(); + // No generation option on username fields. + await showACPopup(2, ["user1"]); + let acEvents = await waitForTelemetryEventsCondition(events => { + return events.length == 1; + }, { process: "parent", filterProps: TelemetryFilterPropsAC, clear: true }, `Wait for there to be 1 autocomplete telemetry event`); + checkACTelemetryEvent(acEvents[0], form.uname, { + "hadPrevious": "0", + "login": "1", + "loginsFooter": "1" + }); + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + await promiseFormsProcessedInSameProcess(); + checkForm(2, "user1", "pass1"); +}); + +add_task(async function test_autofillAutocompletePassword_withGeneration() { + const formAttributesToTest = [ + { + num: 2, + action: "https://autofill", + password: { + name: "password", + autocomplete: "new-password" + } + }, + { + num: 3, + action: "https://autofill", + username: { + name: "username" + }, + password: { + name: "password", + label: "New password" + } + } + ]; + + // 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 formAttributes of formAttributesToTest) { + runInParent(clearGeneratedPasswords); + await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]); + + const formNumber = formAttributes.num; + + const form = createLoginForm(formAttributes); + await promiseFormsProcessedInSameProcess(); + form.reset(); + + // This form should be filled + checkForm(formNumber, "", ""); + + 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"); + checkForm(formNumber, "", "pass1"); + + // 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", + ]); + 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"]); + 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(); + let timePasswordChanged = logins[logins.length - 1].timePasswordChanged; + let time = dateAndTimeFormatter.format(new Date(timePasswordChanged)); + const LABEL_NO_USERNAME = "No username (" + time + ")"; + + let 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", + ]); + expectedACShownTelemetryEvents++; + + 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. + checkForm(formNumber, "", generatedPW); + LOGIN_FIELD_UTILS.checkPasswordMasked(form.password, false, "Second fill of the generated pw"); + + await checkTelemetryEventsPWGenUsed(expectedPWGenTelemetryEvents); + + 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", + ]); + expectedACShownTelemetryEvents++; + + 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 + checkForm(formNumber, "", generatedPW); + // 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_task(async function test_autofillAutocompletePassword_saveLoginDisabled() { + await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = createLoginForm({ + num: 2, + action: "https://autofill", + password: { + name: "password", + autocomplete: "new-password" + } + }); + await promiseFormsProcessedInSameProcess(); + + // form should not be filled + checkForm(2, "", ""); + + let 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 promiseACPopupClosed(); + + await LoginManager.setLoginSavingEnabled(location.origin, true); +}); + +add_task(async function test_deleteAndReselectGeneratedPassword() { + await setStoredLoginsAsync([location.origin, "https://autofill", null, "user1", "pass1"]); + + const form = createLoginForm({ + num: 2, + action: "https://autofill", + password: { + name: "password", + autocomplete: "new-password" + } + }); + await promiseFormsProcessedInSameProcess(); + + info("Removing all logins to test auto-saving of generated passwords"); + await LoginManager.removeAllUserFacingLogins(); + + // form should not be filled + checkForm(2, "", ""); + + 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 storagePromise, menuLabel, itemIndex, savedLogins; + + // fill the password field with the generated password via auto-complete menu + storagePromise = 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 storagePromise; + + form.uname.focus(); + await TestUtils.waitForTick(); + + is(form.password.value.length, LoginTestUtils.generation.LENGTH, "Check password looks generated"); + const GENERATED_PASSWORD = form.password.value; + + savedLogins = await LoginManager.getAllLogins(); + is(savedLogins.length, 1, "Check saved logins count"); + + info("clear the password field and delete the saved login using the AC menu") + storagePromise = 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 storagePromise; + + 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"); +}); + +// add_task(async function test_passwordGenerationShownTelemetry() { +// // Should only be recorded once per principal origin per session, but the cache is cleared each time ``initLogins`` is called. +// await waitForTelemetryEventsCondition(events => { +// return events.length == 3; +// }, { process: "parent", filterProps: TelemetryFilterPropsShown }, "Expect 3 shown telemetry events"); +// }); +</script> +</pre> +</body> +</html> |