summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_password_generation.html
diff options
context:
space:
mode:
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.html574
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>