mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	ContentTask tasks have a different lifetime than SpecialPowers tasks, with the former being tied to the lifetime of a message manager and the latter tied to the lifetime of a window global. That means that existing ContentTask callers which expect to be able to register load listeners before the creation of a window global, or which expect to persist after a page has navigated, won't work as SpecialPowers tasks. Since those sorts of tasks are not really resilient in the face of Fission, they should really be written to work differently, but this patch mostly just reverts them to using ContentTask for the time being. Differential Revision: https://phabricator.services.mozilla.com/D53744 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			719 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			719 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const DIRECTORY_PATH = "/browser/toolkit/components/passwordmgr/test/browser/";
 | 
						|
 | 
						|
ChromeUtils.import("resource://gre/modules/LoginHelper.jsm", this);
 | 
						|
const { LoginManagerParent } = ChromeUtils.import(
 | 
						|
  "resource://gre/modules/LoginManagerParent.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
 | 
						|
ChromeUtils.import("resource://testing-common/ContentTaskUtils.jsm", this);
 | 
						|
ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm", this);
 | 
						|
 | 
						|
add_task(async function common_initialize() {
 | 
						|
  await SpecialPowers.pushPrefEnv({
 | 
						|
    set: [
 | 
						|
      ["signon.rememberSignons", true],
 | 
						|
      ["toolkit.telemetry.ipcBatchTimeout", 0],
 | 
						|
    ],
 | 
						|
  });
 | 
						|
});
 | 
						|
 | 
						|
registerCleanupFunction(
 | 
						|
  async function cleanup_removeAllLoginsAndResetRecipes() {
 | 
						|
    await SpecialPowers.popPrefEnv();
 | 
						|
 | 
						|
    LoginTestUtils.clearData();
 | 
						|
    LoginTestUtils.resetGeneratedPasswordsCache();
 | 
						|
    clearHttpAuths();
 | 
						|
    Services.telemetry.clearEvents();
 | 
						|
 | 
						|
    let recipeParent = LoginTestUtils.recipes.getRecipeParent();
 | 
						|
    if (!recipeParent) {
 | 
						|
      // No need to reset the recipes if the recipe module wasn't even loaded.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    await recipeParent.then(recipeParentResult => recipeParentResult.reset());
 | 
						|
 | 
						|
    await cleanupDoorhanger();
 | 
						|
    let notif;
 | 
						|
    while ((notif = PopupNotifications.getNotification("password"))) {
 | 
						|
      notif.remove();
 | 
						|
    }
 | 
						|
    await closePopup(document.getElementById("contentAreaContextMenu"));
 | 
						|
    await closePopup(document.getElementById("PopupAutoComplete"));
 | 
						|
  }
 | 
						|
);
 | 
						|
 | 
						|
/**
 | 
						|
 * Compared logins in storage to expected values
 | 
						|
 *
 | 
						|
 * @param {array} expectedLogins
 | 
						|
 *        An array of expected login properties
 | 
						|
 * @return {nsILoginInfo[]} - All saved logins sorted by timeCreated
 | 
						|
 */
 | 
						|
function verifyLogins(expectedLogins = []) {
 | 
						|
  let allLogins = Services.logins.getAllLogins();
 | 
						|
  allLogins.sort((a, b) => a.timeCreated > b.timeCreated);
 | 
						|
  is(
 | 
						|
    allLogins.length,
 | 
						|
    expectedLogins.length,
 | 
						|
    "Check actual number of logins matches the number of provided expected property-sets"
 | 
						|
  );
 | 
						|
  for (let i = 0; i < expectedLogins.length; i++) {
 | 
						|
    // if the test doesn't care about comparing properties for this login, just pass false/null.
 | 
						|
    let expected = expectedLogins[i];
 | 
						|
    if (expected) {
 | 
						|
      let login = allLogins[i];
 | 
						|
      if (typeof expected.timesUsed !== "undefined") {
 | 
						|
        is(login.timesUsed, expected.timesUsed, "Check timesUsed");
 | 
						|
      }
 | 
						|
      if (typeof expected.passwordLength !== "undefined") {
 | 
						|
        is(
 | 
						|
          login.password.length,
 | 
						|
          expected.passwordLength,
 | 
						|
          "Check passwordLength"
 | 
						|
        );
 | 
						|
      }
 | 
						|
      if (typeof expected.username !== "undefined") {
 | 
						|
        is(login.username, expected.username, "Check username");
 | 
						|
      }
 | 
						|
      if (typeof expected.password !== "undefined") {
 | 
						|
        is(login.password, expected.password, "Check password");
 | 
						|
      }
 | 
						|
      if (typeof expected.usedSince !== "undefined") {
 | 
						|
        ok(login.timeLastUsed > expected.usedSince, "Check timeLastUsed");
 | 
						|
      }
 | 
						|
      if (typeof expected.passwordChangedSince !== "undefined") {
 | 
						|
        ok(
 | 
						|
          login.timePasswordChanged > expected.passwordChangedSince,
 | 
						|
          "Check timePasswordChanged"
 | 
						|
        );
 | 
						|
      }
 | 
						|
      if (typeof expected.timeCreated !== "undefined") {
 | 
						|
        is(login.timeCreated, expected.timeCreated, "Check timeCreated");
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return allLogins;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Submit the content form and return a promise resolving to the username and
 | 
						|
 * password values echoed out in the response
 | 
						|
 *
 | 
						|
 * @param {Object} [browser] - browser with the form
 | 
						|
 * @param {String = ""} formAction - Optional url to set the form's action to before submitting
 | 
						|
 * @param {Object = null} selectorValues - Optional object with field values to set before form submission
 | 
						|
 * @param {Object = null} responseSelectors - Optional object with selectors to find the username and password in the response
 | 
						|
 */
 | 
						|
async function submitFormAndGetResults(
 | 
						|
  browser,
 | 
						|
  formAction = "",
 | 
						|
  selectorValues,
 | 
						|
  responseSelectors
 | 
						|
) {
 | 
						|
  async function contentSubmitForm([contentFormAction, contentSelectorValues]) {
 | 
						|
    const { WrapPrivileged } = ChromeUtils.import(
 | 
						|
      "resource://specialpowers/WrapPrivileged.jsm",
 | 
						|
      this
 | 
						|
    );
 | 
						|
    let doc = content.document;
 | 
						|
    let form = doc.querySelector("form");
 | 
						|
    if (contentFormAction) {
 | 
						|
      form.action = contentFormAction;
 | 
						|
    }
 | 
						|
    for (let [sel, value] of Object.entries(contentSelectorValues)) {
 | 
						|
      try {
 | 
						|
        let field = doc.querySelector(sel);
 | 
						|
        let gotInput = ContentTaskUtils.waitForEvent(
 | 
						|
          field,
 | 
						|
          "input",
 | 
						|
          "Got input event on " + sel
 | 
						|
        );
 | 
						|
        // we don't get an input event if the new value == the old
 | 
						|
        field.value = "###";
 | 
						|
        WrapPrivileged.wrap(field).setUserInput(value);
 | 
						|
        await gotInput;
 | 
						|
      } catch (ex) {
 | 
						|
        throw new Error(
 | 
						|
          `submitForm: Couldn't set value of field at: ${sel}: ${ex.message}`
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
    form.submit();
 | 
						|
  }
 | 
						|
  await SpecialPowers.spawn(
 | 
						|
    browser,
 | 
						|
    [[formAction, selectorValues]],
 | 
						|
    contentSubmitForm
 | 
						|
  );
 | 
						|
  let result = await getFormSubmitResponseResult(
 | 
						|
    browser,
 | 
						|
    formAction,
 | 
						|
    responseSelectors
 | 
						|
  );
 | 
						|
  return result;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Wait for a given result page to load and return a promise resolving to an object with the parsed-out
 | 
						|
 * username/password values from the response
 | 
						|
 *
 | 
						|
 * @param {Object} [browser] - browser which is loading this page
 | 
						|
 * @param {String} resultURL - the path or filename to look for in the content.location
 | 
						|
 * @param {Object = null} - Optional object with selectors to find the username and password in the response
 | 
						|
 */
 | 
						|
async function getFormSubmitResponseResult(
 | 
						|
  browser,
 | 
						|
  resultURL = "/formsubmit.sjs",
 | 
						|
  { username = "#user", password = "#pass" } = {}
 | 
						|
) {
 | 
						|
  // default selectors are for the response page produced by formsubmit.sjs
 | 
						|
  let fieldValues = await ContentTask.spawn(
 | 
						|
    browser,
 | 
						|
    {
 | 
						|
      resultURL,
 | 
						|
      usernameSelector: username,
 | 
						|
      passwordSelector: password,
 | 
						|
    },
 | 
						|
    async function({ resultURL, usernameSelector, passwordSelector }) {
 | 
						|
      await ContentTaskUtils.waitForCondition(() => {
 | 
						|
        return (
 | 
						|
          content.location.pathname.endsWith(resultURL) &&
 | 
						|
          content.document.readyState == "complete"
 | 
						|
        );
 | 
						|
      }, `Wait for form submission load (${resultURL})`);
 | 
						|
      let username = content.document.querySelector(usernameSelector)
 | 
						|
        .textContent;
 | 
						|
      let password = content.document.querySelector(passwordSelector)
 | 
						|
        .textContent;
 | 
						|
      return {
 | 
						|
        username,
 | 
						|
        password,
 | 
						|
      };
 | 
						|
    }
 | 
						|
  );
 | 
						|
  return fieldValues;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Loads a test page in `DIRECTORY_URL` which automatically submits to formsubmit.sjs and returns a
 | 
						|
 * promise resolving with the field values when the optional `aTaskFn` is done.
 | 
						|
 *
 | 
						|
 * @param {String} aPageFile - test page file name which auto-submits to formsubmit.sjs
 | 
						|
 * @param {Function} aTaskFn - task which can be run before the tab closes.
 | 
						|
 * @param {String} [aOrigin="http://example.com"] - origin of the server to use
 | 
						|
 *                                                  to load `aPageFile`.
 | 
						|
 */
 | 
						|
function testSubmittingLoginForm(
 | 
						|
  aPageFile,
 | 
						|
  aTaskFn,
 | 
						|
  aOrigin = "http://example.com"
 | 
						|
) {
 | 
						|
  return BrowserTestUtils.withNewTab(
 | 
						|
    {
 | 
						|
      gBrowser,
 | 
						|
      url: aOrigin + DIRECTORY_PATH + aPageFile,
 | 
						|
    },
 | 
						|
    async function(browser) {
 | 
						|
      ok(true, "loaded " + aPageFile);
 | 
						|
      let fieldValues = await getFormSubmitResponseResult(
 | 
						|
        browser,
 | 
						|
        "/formsubmit.sjs"
 | 
						|
      );
 | 
						|
      ok(true, "form submission loaded");
 | 
						|
      if (aTaskFn) {
 | 
						|
        await aTaskFn(fieldValues);
 | 
						|
      }
 | 
						|
      return fieldValues;
 | 
						|
    }
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
function checkOnlyLoginWasUsedTwice({ justChanged }) {
 | 
						|
  // Check to make sure we updated the timestamps and use count on the
 | 
						|
  // existing login that was submitted for the test.
 | 
						|
  let logins = Services.logins.getAllLogins();
 | 
						|
  is(logins.length, 1, "Should only have 1 login");
 | 
						|
  ok(logins[0] instanceof Ci.nsILoginMetaInfo, "metainfo QI");
 | 
						|
  is(logins[0].timesUsed, 2, "check .timesUsed for existing login submission");
 | 
						|
  ok(logins[0].timeCreated < logins[0].timeLastUsed, "timeLastUsed bumped");
 | 
						|
  if (justChanged) {
 | 
						|
    is(
 | 
						|
      logins[0].timeLastUsed,
 | 
						|
      logins[0].timePasswordChanged,
 | 
						|
      "timeLastUsed == timePasswordChanged"
 | 
						|
    );
 | 
						|
  } else {
 | 
						|
    is(
 | 
						|
      logins[0].timeCreated,
 | 
						|
      logins[0].timePasswordChanged,
 | 
						|
      "timeChanged not updated"
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function clearHttpAuths() {
 | 
						|
  let authMgr = Cc["@mozilla.org/network/http-auth-manager;1"].getService(
 | 
						|
    Ci.nsIHttpAuthManager
 | 
						|
  );
 | 
						|
  authMgr.clearAll();
 | 
						|
}
 | 
						|
 | 
						|
// Begin popup notification (doorhanger) functions //
 | 
						|
 | 
						|
const REMEMBER_BUTTON = "button";
 | 
						|
const NEVER_MENUITEM = 0;
 | 
						|
 | 
						|
const CHANGE_BUTTON = "button";
 | 
						|
const DONT_CHANGE_BUTTON = "secondaryButton";
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks if we have a password capture popup notification
 | 
						|
 * of the right type and with the right label.
 | 
						|
 *
 | 
						|
 * @param {String} aKind The desired `passwordNotificationType`
 | 
						|
 * @param {Object} [popupNotifications = PopupNotifications]
 | 
						|
 * @param {Object} [browser = null] Optional browser whose notifications should be searched.
 | 
						|
 * @return the found password popup notification.
 | 
						|
 */
 | 
						|
function getCaptureDoorhanger(
 | 
						|
  aKind,
 | 
						|
  popupNotifications = PopupNotifications,
 | 
						|
  browser = null
 | 
						|
) {
 | 
						|
  ok(true, "Looking for " + aKind + " popup notification");
 | 
						|
  let notification = popupNotifications.getNotification("password", browser);
 | 
						|
  if (notification) {
 | 
						|
    is(
 | 
						|
      notification.options.passwordNotificationType,
 | 
						|
      aKind,
 | 
						|
      "Notification type matches."
 | 
						|
    );
 | 
						|
    if (aKind == "password-change") {
 | 
						|
      is(
 | 
						|
        notification.mainAction.label,
 | 
						|
        "Update",
 | 
						|
        "Main action label matches update doorhanger."
 | 
						|
      );
 | 
						|
    } else if (aKind == "password-save") {
 | 
						|
      is(
 | 
						|
        notification.mainAction.label,
 | 
						|
        "Save",
 | 
						|
        "Main action label matches save doorhanger."
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return notification;
 | 
						|
}
 | 
						|
 | 
						|
async function getCaptureDoorhangerThatMayOpen(
 | 
						|
  aKind,
 | 
						|
  popupNotifications = PopupNotifications,
 | 
						|
  browser = null
 | 
						|
) {
 | 
						|
  let notif = getCaptureDoorhanger(aKind, popupNotifications, browser);
 | 
						|
  if (notif && !notif.dismissed) {
 | 
						|
    if (popupNotifications.panel.state !== "open") {
 | 
						|
      await BrowserTestUtils.waitForEvent(
 | 
						|
        popupNotifications.panel,
 | 
						|
        "popupshown"
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return notif;
 | 
						|
}
 | 
						|
 | 
						|
async function waitForDoorhanger(browser, type) {
 | 
						|
  await TestUtils.waitForCondition(() => {
 | 
						|
    let notif = PopupNotifications.getNotification("password", browser);
 | 
						|
    return notif && notif.options.passwordNotificationType == type;
 | 
						|
  }, `Waiting for a ${type} notification`);
 | 
						|
}
 | 
						|
 | 
						|
async function hideDoorhangerPopup() {
 | 
						|
  info("hideDoorhangerPopup");
 | 
						|
  if (!PopupNotifications.isPanelOpen) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  let { panel } = PopupNotifications;
 | 
						|
  let promiseHidden = BrowserTestUtils.waitForEvent(panel, "popuphidden");
 | 
						|
  panel.hidePopup();
 | 
						|
  await promiseHidden;
 | 
						|
  info("got popuphidden from notification panel");
 | 
						|
}
 | 
						|
 | 
						|
function getDoorhangerButton(aPopup, aButtonIndex) {
 | 
						|
  let notifications = aPopup.owner.panel.children;
 | 
						|
  ok(!!notifications.length, "at least one notification displayed");
 | 
						|
  ok(true, notifications.length + " notification(s)");
 | 
						|
  let notification = notifications[0];
 | 
						|
 | 
						|
  if (aButtonIndex == "button") {
 | 
						|
    return notification.button;
 | 
						|
  } else if (aButtonIndex == "secondaryButton") {
 | 
						|
    return notification.secondaryButton;
 | 
						|
  }
 | 
						|
  return notification.menupopup.querySelectorAll("menuitem")[aButtonIndex];
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Clicks the specified popup notification button.
 | 
						|
 *
 | 
						|
 * @param {Element} aPopup Popup Notification element
 | 
						|
 * @param {Number} aButtonIndex Number indicating which button to click.
 | 
						|
 *                              See the constants in this file.
 | 
						|
 */
 | 
						|
function clickDoorhangerButton(aPopup, aButtonIndex) {
 | 
						|
  ok(true, "Looking for action at index " + aButtonIndex);
 | 
						|
 | 
						|
  let button = getDoorhangerButton(aPopup, aButtonIndex);
 | 
						|
  if (aButtonIndex == "button") {
 | 
						|
    ok(true, "Triggering main action");
 | 
						|
  } else if (aButtonIndex == "secondaryButton") {
 | 
						|
    ok(true, "Triggering secondary action");
 | 
						|
  } else {
 | 
						|
    ok(true, "Triggering menuitem # " + aButtonIndex);
 | 
						|
  }
 | 
						|
  button.doCommand();
 | 
						|
}
 | 
						|
 | 
						|
async function cleanupDoorhanger(notif) {
 | 
						|
  let PN = notif ? notif.owner : PopupNotifications;
 | 
						|
  if (notif) {
 | 
						|
    notif.remove();
 | 
						|
  }
 | 
						|
  let promiseHidden = PN.isPanelOpen
 | 
						|
    ? BrowserTestUtils.waitForEvent(PN.panel, "popuphidden")
 | 
						|
    : Promise.resolve();
 | 
						|
  PN.panel.hidePopup();
 | 
						|
  await promiseHidden;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Checks the doorhanger's username and password.
 | 
						|
 *
 | 
						|
 * @param {String} username The username.
 | 
						|
 * @param {String} password The password.
 | 
						|
 */
 | 
						|
async function checkDoorhangerUsernamePassword(username, password) {
 | 
						|
  await BrowserTestUtils.waitForCondition(() => {
 | 
						|
    return (
 | 
						|
      document.getElementById("password-notification-username").value ==
 | 
						|
      username
 | 
						|
    );
 | 
						|
  }, "Wait for nsLoginManagerPrompter writeDataToUI()");
 | 
						|
  is(
 | 
						|
    document.getElementById("password-notification-username").value,
 | 
						|
    username,
 | 
						|
    "Check doorhanger username"
 | 
						|
  );
 | 
						|
  is(
 | 
						|
    document.getElementById("password-notification-password").value,
 | 
						|
    password,
 | 
						|
    "Check doorhanger password"
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Change the doorhanger's username and password input values.
 | 
						|
 *
 | 
						|
 * @param {object} newValues
 | 
						|
 *        named values to update
 | 
						|
 * @param {string} [newValues.password = undefined]
 | 
						|
 *        An optional string value to replace whatever is in the password field
 | 
						|
 * @param {string} [newValues.username = undefined]
 | 
						|
 *        An optional string value to replace whatever is in the username field
 | 
						|
 * @param {Object} [popupNotifications = PopupNotifications]
 | 
						|
 */
 | 
						|
async function updateDoorhangerInputValues(
 | 
						|
  newValues,
 | 
						|
  popupNotifications = PopupNotifications
 | 
						|
) {
 | 
						|
  let { panel } = popupNotifications;
 | 
						|
  if (popupNotifications.panel.state !== "open") {
 | 
						|
    await BrowserTestUtils.waitForEvent(popupNotifications.panel, "popupshown");
 | 
						|
  }
 | 
						|
  is(panel.state, "open", "Check the doorhanger is already open");
 | 
						|
 | 
						|
  let notifElem = panel.childNodes[0];
 | 
						|
 | 
						|
  // Note: setUserInput does not reliably dispatch input events from chrome elements?
 | 
						|
  async function setInputValue(target, value) {
 | 
						|
    info(`setInputValue: on target: ${target.id}, value: ${value}`);
 | 
						|
    target.focus();
 | 
						|
    target.select();
 | 
						|
    await EventUtils.synthesizeKey("KEY_Backspace");
 | 
						|
    info(
 | 
						|
      `setInputValue: target.value: ${target.value}, sending new value string`
 | 
						|
    );
 | 
						|
    await EventUtils.sendString(value);
 | 
						|
    await EventUtils.synthesizeKey("KEY_Tab");
 | 
						|
    return Promise.resolve();
 | 
						|
  }
 | 
						|
 | 
						|
  let passwordField = notifElem.querySelector(
 | 
						|
    "#password-notification-password"
 | 
						|
  );
 | 
						|
  let usernameField = notifElem.querySelector(
 | 
						|
    "#password-notification-username"
 | 
						|
  );
 | 
						|
 | 
						|
  if (typeof newValues.password !== "undefined") {
 | 
						|
    if (passwordField.value !== newValues.password) {
 | 
						|
      await setInputValue(passwordField, newValues.password);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  if (typeof newValues.username !== "undefined") {
 | 
						|
    if (usernameField.value !== newValues.username) {
 | 
						|
      await setInputValue(usernameField, newValues.username);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// End popup notification (doorhanger) functions //
 | 
						|
 | 
						|
async function waitForPasswordManagerDialog(openingFunc) {
 | 
						|
  let win;
 | 
						|
  await openingFunc();
 | 
						|
  await TestUtils.waitForCondition(() => {
 | 
						|
    win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
 | 
						|
    return win && win.document.getElementById("filter");
 | 
						|
  }, "Waiting for the password manager dialog to open");
 | 
						|
 | 
						|
  return {
 | 
						|
    filterValue: win.document.getElementById("filter").value,
 | 
						|
    async close() {
 | 
						|
      await BrowserTestUtils.closeWindow(win);
 | 
						|
    },
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
async function waitForPasswordManagerTab(openingFunc, waitForFilter) {
 | 
						|
  info("waiting for new tab to open");
 | 
						|
  let tabPromise = BrowserTestUtils.waitForNewTab(
 | 
						|
    gBrowser,
 | 
						|
    url => url.includes("about:logins") && !url.includes("entryPoint=")
 | 
						|
  );
 | 
						|
  await openingFunc();
 | 
						|
  let tab = await tabPromise;
 | 
						|
  ok(tab, "got password management tab");
 | 
						|
  let filterValue;
 | 
						|
  if (waitForFilter) {
 | 
						|
    filterValue = await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
 | 
						|
      let loginFilter = Cu.waiveXrays(
 | 
						|
        content.document.querySelector("login-filter")
 | 
						|
      );
 | 
						|
      await ContentTaskUtils.waitForCondition(
 | 
						|
        () => !!loginFilter.value,
 | 
						|
        "wait for login-filter to have a value"
 | 
						|
      );
 | 
						|
      return loginFilter.value;
 | 
						|
    });
 | 
						|
  }
 | 
						|
  return {
 | 
						|
    filterValue,
 | 
						|
    close() {
 | 
						|
      BrowserTestUtils.removeTab(tab);
 | 
						|
    },
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
function openPasswordManager(openingFunc, waitForFilter) {
 | 
						|
  return Services.prefs.getCharPref("signon.management.overrideURI")
 | 
						|
    ? waitForPasswordManagerTab(openingFunc, waitForFilter)
 | 
						|
    : waitForPasswordManagerDialog(openingFunc);
 | 
						|
}
 | 
						|
 | 
						|
// Autocomplete popup related functions //
 | 
						|
 | 
						|
async function openACPopup(popup, browser, inputSelector) {
 | 
						|
  let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
 | 
						|
 | 
						|
  await SimpleTest.promiseFocus(browser);
 | 
						|
  info("content window focused");
 | 
						|
 | 
						|
  // Focus the username field to open the popup.
 | 
						|
  await SpecialPowers.spawn(
 | 
						|
    browser,
 | 
						|
    [[inputSelector]],
 | 
						|
    function openAutocomplete(sel) {
 | 
						|
      content.document.querySelector(sel).focus();
 | 
						|
    }
 | 
						|
  );
 | 
						|
 | 
						|
  let shown = await promiseShown;
 | 
						|
  ok(shown, "autocomplete popup shown");
 | 
						|
  return shown;
 | 
						|
}
 | 
						|
 | 
						|
async function closePopup(popup) {
 | 
						|
  if (popup.state == "closed") {
 | 
						|
    await Promise.resolve();
 | 
						|
  } else {
 | 
						|
    let promiseHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
 | 
						|
    popup.hidePopup();
 | 
						|
    await promiseHidden;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
async function fillGeneratedPasswordFromOpenACPopup(
 | 
						|
  browser,
 | 
						|
  passwordInputSelector
 | 
						|
) {
 | 
						|
  let popup = browser.ownerDocument.getElementById("PopupAutoComplete");
 | 
						|
  let item;
 | 
						|
 | 
						|
  await TestUtils.waitForCondition(() => {
 | 
						|
    item = popup.querySelector(`[originaltype="generatedPassword"]`);
 | 
						|
    return item && !EventUtils.isHidden(item);
 | 
						|
  }, "Waiting for item to become visible");
 | 
						|
 | 
						|
  let inputEventPromise = ContentTask.spawn(
 | 
						|
    browser,
 | 
						|
    [passwordInputSelector],
 | 
						|
    async function waitForInput(inputSelector) {
 | 
						|
      let passwordInput = content.document.querySelector(inputSelector);
 | 
						|
      await ContentTaskUtils.waitForEvent(
 | 
						|
        passwordInput,
 | 
						|
        "input",
 | 
						|
        "Password input value changed"
 | 
						|
      );
 | 
						|
    }
 | 
						|
  );
 | 
						|
 | 
						|
  let passwordGeneratedPromise = listenForTestNotification(
 | 
						|
    "PasswordFilledOrEdited"
 | 
						|
  );
 | 
						|
 | 
						|
  info("Clicking the generated password AC item");
 | 
						|
  EventUtils.synthesizeMouseAtCenter(item, {});
 | 
						|
  info("Waiting for the content input value to change");
 | 
						|
  await inputEventPromise;
 | 
						|
  await passwordGeneratedPromise;
 | 
						|
}
 | 
						|
 | 
						|
// Contextmenu functions //
 | 
						|
 | 
						|
/**
 | 
						|
 * Synthesize mouse clicks to open the password manager context menu popup
 | 
						|
 * for a target password input element.
 | 
						|
 *
 | 
						|
 * assertCallback should return true if we should continue or else false.
 | 
						|
 */
 | 
						|
async function openPasswordContextMenu(
 | 
						|
  browser,
 | 
						|
  passwordInput,
 | 
						|
  assertCallback = null,
 | 
						|
  browsingContext = null
 | 
						|
) {
 | 
						|
  const doc = browser.ownerDocument;
 | 
						|
  const CONTEXT_MENU = doc.getElementById("contentAreaContextMenu");
 | 
						|
  const POPUP_HEADER = doc.getElementById("fill-login");
 | 
						|
  const LOGIN_POPUP = doc.getElementById("fill-login-popup");
 | 
						|
 | 
						|
  if (!browsingContext) {
 | 
						|
    browsingContext = browser.browsingContext;
 | 
						|
  }
 | 
						|
 | 
						|
  let contextMenuShownPromise = BrowserTestUtils.waitForEvent(
 | 
						|
    CONTEXT_MENU,
 | 
						|
    "popupshown"
 | 
						|
  );
 | 
						|
 | 
						|
  // Synthesize a right mouse click over the password input element, we have to trigger
 | 
						|
  // both events because formfill code relies on this event happening before the contextmenu
 | 
						|
  // (which it does for real user input) in order to not show the password autocomplete.
 | 
						|
  let eventDetails = { type: "mousedown", button: 2 };
 | 
						|
  await BrowserTestUtils.synthesizeMouseAtCenter(
 | 
						|
    passwordInput,
 | 
						|
    eventDetails,
 | 
						|
    browsingContext
 | 
						|
  );
 | 
						|
  // Synthesize a contextmenu event to actually open the context menu.
 | 
						|
  eventDetails = { type: "contextmenu", button: 2 };
 | 
						|
  await BrowserTestUtils.synthesizeMouseAtCenter(
 | 
						|
    passwordInput,
 | 
						|
    eventDetails,
 | 
						|
    browsingContext
 | 
						|
  );
 | 
						|
 | 
						|
  await contextMenuShownPromise;
 | 
						|
 | 
						|
  if (assertCallback) {
 | 
						|
    let shouldContinue = await assertCallback();
 | 
						|
    if (!shouldContinue) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Synthesize a mouse click over the fill login menu header.
 | 
						|
  let popupShownPromise = BrowserTestUtils.waitForCondition(
 | 
						|
    () => POPUP_HEADER.open && BrowserTestUtils.is_visible(LOGIN_POPUP)
 | 
						|
  );
 | 
						|
  EventUtils.synthesizeMouseAtCenter(POPUP_HEADER, {}, browser.ownerGlobal);
 | 
						|
  await popupShownPromise;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Listen for the login manager test notification specified by
 | 
						|
 * expectedMessage. Possible messages:
 | 
						|
 *   FormProcessed - a form was processed after page load.
 | 
						|
 *   FormSubmit - a form was just submitted.
 | 
						|
 *   PasswordFilledOrEdited - a password was filled in or modified.
 | 
						|
 *
 | 
						|
 * The count is the number of that messages to wait for. This should
 | 
						|
 * typically be used when waiting for the FormProcessed message for a page
 | 
						|
 * that has subframes to ensure all have been handled.
 | 
						|
 *
 | 
						|
 * Returns a promise that will passed additional data specific to the message.
 | 
						|
 */
 | 
						|
function listenForTestNotification(expectedMessage, count = 1) {
 | 
						|
  return new Promise(resolve => {
 | 
						|
    LoginManagerParent.setListenerForTests((msg, data) => {
 | 
						|
      if (msg == expectedMessage && --count == 0) {
 | 
						|
        LoginManagerParent.setListenerForTests(null);
 | 
						|
        resolve(data);
 | 
						|
      }
 | 
						|
    });
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Use the contextmenu to fill a field with a generated password
 | 
						|
 */
 | 
						|
async function doFillGeneratedPasswordContextMenuItem(browser, passwordInput) {
 | 
						|
  await SimpleTest.promiseFocus(browser);
 | 
						|
  await openPasswordContextMenu(browser, passwordInput);
 | 
						|
 | 
						|
  let loginPopup = document.getElementById("fill-login-popup");
 | 
						|
  let generatedPasswordItem = document.getElementById(
 | 
						|
    "fill-login-generated-password"
 | 
						|
  );
 | 
						|
  let generatedPasswordSeparator = document.getElementById(
 | 
						|
    "generated-password-separator"
 | 
						|
  );
 | 
						|
 | 
						|
  // Check the content of the password manager popup
 | 
						|
  ok(BrowserTestUtils.is_visible(loginPopup), "Popup is visible");
 | 
						|
  ok(
 | 
						|
    BrowserTestUtils.is_visible(generatedPasswordItem),
 | 
						|
    "generated password item is visible"
 | 
						|
  );
 | 
						|
  ok(
 | 
						|
    BrowserTestUtils.is_visible(generatedPasswordSeparator),
 | 
						|
    "separator is visible"
 | 
						|
  );
 | 
						|
 | 
						|
  let popup = document.getElementById("PopupAutoComplete");
 | 
						|
  ok(popup, "Got popup");
 | 
						|
  let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
 | 
						|
 | 
						|
  await new Promise(resolve => {
 | 
						|
    SimpleTest.executeSoon(resolve);
 | 
						|
  });
 | 
						|
 | 
						|
  EventUtils.synthesizeMouseAtCenter(generatedPasswordItem, {});
 | 
						|
 | 
						|
  await promiseShown;
 | 
						|
  await fillGeneratedPasswordFromOpenACPopup(browser, passwordInput);
 | 
						|
}
 |