Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Errors

/**
* Test the password manager context menu.
*/
/* eslint no-shadow:"off" */
"use strict";
// The origin for the test URIs.
const TEST_ORIGIN = "https://example.com";
const MULTIPLE_FORMS_PAGE_PATH =
"/browser/toolkit/components/passwordmgr/test/browser/multiple_forms.html";
const CONTEXT_MENU = document.getElementById("contentAreaContextMenu");
const POPUP_HEADER = document.getElementById("fill-login");
/**
* Initialize logins needed for the tests and disable autofill
* for login forms for easier testing of manual fill.
*/
add_task(async function test_initialize() {
Services.prefs.setBoolPref("signon.autofillForms", false);
Services.prefs.setBoolPref("signon.usernameOnlyForm.enabled", true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("signon.autofillForms");
Services.prefs.clearUserPref("signon.schemeUpgrades");
Services.prefs.clearUserPref("signon.usernameOnlyForm.enabled");
});
await Services.logins.addLogins(loginList());
});
/**
* Check if the context menu is populated with the right
* menuitems for the target password input field.
*/
add_task(async function test_context_menu_populate_password_noSchemeUpgrades() {
Services.prefs.setBoolPref("signon.schemeUpgrades", false);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
},
async function (browser) {
await openPasswordContextMenu(browser, "#test-password-1");
// Check the content of the password manager popup
let popupMenu = document.getElementById("fill-login-popup");
checkMenu(popupMenu, 2);
await closePopup(CONTEXT_MENU);
}
);
});
/**
* Check if the context menu is populated with the right
* menuitems for the target password input field.
*/
add_task(async function test_context_menu_populate_password_schemeUpgrades() {
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
},
async function (browser) {
await openPasswordContextMenu(browser, "#test-password-1");
// Check the content of the password manager popup
let popupMenu = document.getElementById("fill-login-popup");
checkMenu(popupMenu, 3);
await closePopup(CONTEXT_MENU);
}
);
});
/**
* Check if the context menu is populated with the right menuitems
* for the target username field with a password field present.
*/
add_task(
async function test_context_menu_populate_username_with_password_noSchemeUpgrades() {
Services.prefs.setBoolPref("signon.schemeUpgrades", false);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url:
TEST_ORIGIN +
"/browser/toolkit/components/" +
"passwordmgr/test/browser/multiple_forms.html",
},
async function (browser) {
await openPasswordContextMenu(browser, "#test-username-3");
// Check the content of the password manager popup
let popupMenu = document.getElementById("fill-login-popup");
checkMenu(popupMenu, 2);
await closePopup(CONTEXT_MENU);
}
);
}
);
/**
* Check if the context menu is populated with the right menuitems
* for the target username field with a password field present.
*/
add_task(
async function test_context_menu_populate_username_with_password_schemeUpgrades() {
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url:
TEST_ORIGIN +
"/browser/toolkit/components/" +
"passwordmgr/test/browser/multiple_forms.html",
},
async function (browser) {
await openPasswordContextMenu(browser, "#test-username-3");
// Check the content of the password manager popup
let popupMenu = document.getElementById("fill-login-popup");
checkMenu(popupMenu, 3);
await closePopup(CONTEXT_MENU);
}
);
}
);
/**
* Check if the context menu is populated with the right menuitems
* for the target username field without a password field present.
*/
add_task(
async function test_context_menu_populate_username_with_password_noSchemeUpgrades() {
Services.prefs.setBoolPref("signon.schemeUpgrades", false);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url:
TEST_ORIGIN +
"/browser/toolkit/components/" +
"passwordmgr/test/browser/multiple_forms.html",
},
async function (browser) {
await openPasswordContextMenu(browser, "#test-username-1");
// Check the content of the password manager popup
let popupMenu = document.getElementById("fill-login-popup");
checkMenu(popupMenu, 2);
await closePopup(CONTEXT_MENU);
}
);
}
);
/**
* Check if the context menu is populated with the right menuitems
* for the target username field without a password field present.
*/
add_task(
async function test_context_menu_populate_username_with_password_schemeUpgrades() {
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url:
TEST_ORIGIN +
"/browser/toolkit/components/" +
"passwordmgr/test/browser/multiple_forms.html",
},
async function (browser) {
await openPasswordContextMenu(browser, "#test-username-1");
// Check the content of the password manager popup
let popupMenu = document.getElementById("fill-login-popup");
checkMenu(popupMenu, 3);
await closePopup(CONTEXT_MENU);
}
);
}
);
/**
* Check if the password field is correctly filled when one
* login menuitem is clicked.
*/
add_task(async function test_context_menu_password_fill() {
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
},
async function (browser) {
let formDescriptions = await SpecialPowers.spawn(
browser,
[],
async function () {
let forms = Array.from(
content.document.getElementsByClassName("test-form")
);
return forms.map(f => f.getAttribute("description"));
}
);
for (let description of formDescriptions) {
info("Testing form: " + description);
let passwordInputIds = await SpecialPowers.spawn(
browser,
[{ description }],
async function ({ description }) {
let formElement = content.document.querySelector(
`[description="${description}"]`
);
let passwords = Array.from(
formElement.querySelectorAll(
"input[type='password'], input[data-type='password']"
)
);
return passwords.map(p => p.id);
}
);
for (let inputId of passwordInputIds) {
info("Testing password field: " + inputId);
// Synthesize a right mouse click over the password input element.
await openPasswordContextMenu(
browser,
"#" + inputId,
async function () {
let inputDisabled = await SpecialPowers.spawn(
browser,
[{ inputId }],
async function ({ inputId }) {
let input = content.document.getElementById(inputId);
return input.disabled || input.readOnly;
}
);
// If the password field is disabled or read-only, we want to see
// the disabled Fill Password popup header.
if (inputDisabled) {
Assert.ok(!POPUP_HEADER.hidden, "Popup menu is not hidden.");
Assert.ok(POPUP_HEADER.disabled, "Popup menu is disabled.");
await closePopup(CONTEXT_MENU);
}
Assert.equal(
POPUP_HEADER.getAttribute("data-l10n-id"),
"main-context-menu-use-saved-password",
"top-level label is correct"
);
return !inputDisabled;
}
);
if (CONTEXT_MENU.state != "open") {
continue;
}
// The only field affected by the password fill
// should be the target password field itself.
await assertContextMenuFill(browser, description, null, inputId, 1);
await SpecialPowers.spawn(
browser,
[{ inputId }],
async function ({ inputId }) {
let passwordField = content.document.getElementById(inputId);
Assert.equal(
passwordField.value,
"password1",
"Check upgraded login was actually used"
);
}
);
await closePopup(CONTEXT_MENU);
}
}
}
);
});
/**
* Check if the form is correctly filled when one
* username context menu login menuitem is clicked.
*/
add_task(async function test_context_menu_username_login_fill() {
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
},
async function (browser) {
let formDescriptions = await SpecialPowers.spawn(
browser,
[],
async function () {
let forms = Array.from(
content.document.getElementsByClassName("test-form")
);
return forms.map(f => f.getAttribute("description"));
}
);
for (let description of formDescriptions) {
info("Testing form: " + description);
let usernameInputIds = await SpecialPowers.spawn(
browser,
[{ description }],
async function ({ description }) {
let formElement = content.document.querySelector(
`[description="${description}"]`
);
let inputs = Array.from(
formElement.querySelectorAll(
"input[type='text']:not([data-type='password'])"
)
);
return inputs.map(p => p.id);
}
);
for (let inputId of usernameInputIds) {
info("Testing username field: " + inputId);
// Synthesize a right mouse click over the username input element.
await openPasswordContextMenu(
browser,
"#" + inputId,
async function () {
let headerHidden = POPUP_HEADER.hidden;
let headerDisabled = POPUP_HEADER.disabled;
let headerLabelID = POPUP_HEADER.getAttribute("data-l10n-id");
let data = {
description,
inputId,
headerHidden,
headerDisabled,
headerLabelID,
};
let shouldContinue = await SpecialPowers.spawn(
browser,
[data],
async function (data) {
let {
description,
inputId,
headerHidden,
headerDisabled,
headerLabelID,
} = data;
let formElement = content.document.querySelector(
`[description="${description}"]`
);
let usernameField = content.document.getElementById(inputId);
// We always want to check if the first password field is filled,
// since this is the current behavior from the _fillForm function.
let passwordField = formElement.querySelector(
"input[type='password'], input[data-type='password']"
);
// If we don't want to see the actual popup menu,
// check if the popup is hidden or disabled.
if (
!passwordField ||
usernameField.disabled ||
usernameField.readOnly ||
passwordField.disabled ||
passwordField.readOnly
) {
if (!passwordField) {
// Should show popup for a username-only form.
if (usernameField.autocomplete == "username") {
Assert.ok(!headerHidden, "Popup menu is not hidden.");
} else {
Assert.ok(headerHidden, "Popup menu is hidden.");
}
} else {
Assert.ok(!headerHidden, "Popup menu is not hidden.");
Assert.ok(headerDisabled, "Popup menu is disabled.");
}
return false;
}
Assert.equal(
headerLabelID,
"main-context-menu-use-saved-password",
"top-level label is correct"
);
return true;
}
);
if (!shouldContinue) {
await closePopup(CONTEXT_MENU);
}
return shouldContinue;
}
);
if (CONTEXT_MENU.state != "open") {
continue;
}
let passwordFieldId = await SpecialPowers.spawn(
browser,
[{ description }],
async function ({ description }) {
let formElement = content.document.querySelector(
`[description="${description}"]`
);
return formElement.querySelector(
"input[type='password'], input[data-type='password']"
).id;
}
);
// We shouldn't change any field that's not the target username field or the first password field
await assertContextMenuFill(
browser,
description,
inputId,
passwordFieldId,
1
);
await SpecialPowers.spawn(
browser,
[{ passwordFieldId }],
async function ({ passwordFieldId }) {
let passwordField =
content.document.getElementById(passwordFieldId);
if (!passwordField.hasAttribute("expectedFail")) {
Assert.equal(
passwordField.value,
"password1",
"Check upgraded login was actually used"
);
}
}
);
await closePopup(CONTEXT_MENU);
}
}
}
);
});
/**
* Check event telemetry is correctly recorded when opening the saved logins / management UI
* from the context menu
*/
add_task(async function test_context_menu_open_management() {
Services.prefs.setBoolPref("signon.schemeUpgrades", false);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: TEST_ORIGIN + MULTIPLE_FORMS_PAGE_PATH,
},
async function (browser) {
await openPasswordContextMenu(browser, "#test-password-1");
let openingFunc = () => gContextMenu.openPasswordManager();
// wait until the management UI opens
let passwordManager = await openPasswordManager(openingFunc);
info("Management UI dialog was opened");
TelemetryTestUtils.assertEvents(
[["pwmgr", "open_management", "contextmenu"]],
{ category: "pwmgr", method: "open_management" },
{ clear: true, process: "content" }
);
await passwordManager.close();
await closePopup(CONTEXT_MENU);
}
);
});
/**
* Verify that only the expected form fields are filled.
*/
async function assertContextMenuFill(
browser,
formId,
usernameFieldId,
passwordFieldId,
loginIndex
) {
let popupMenu = document.getElementById("fill-login-popup");
let unchangedSelector = `[description="${formId}"] input:not(#${passwordFieldId})`;
if (usernameFieldId) {
unchangedSelector += `:not(#${usernameFieldId})`;
}
await SpecialPowers.spawn(
browser,
[{ unchangedSelector }],
async function ({ unchangedSelector }) {
let unchangedFields =
content.document.querySelectorAll(unchangedSelector);
// Store the value of fields that should remain unchanged.
if (unchangedFields.length) {
for (let field of unchangedFields) {
field.setAttribute("original-value", field.value);
}
}
}
);
// Execute the default command of the specified login menuitem found in the context menu.
let loginItem =
popupMenu.getElementsByClassName("context-login-item")[loginIndex];
// Find the used login by it's username (Use only unique usernames in this test).
let { username, password } = getLoginFromUsername(loginItem.label);
let data = {
username,
password,
usernameFieldId,
passwordFieldId,
formId,
unchangedSelector,
};
let continuePromise = ContentTask.spawn(browser, data, async function (data) {
let {
username,
password,
usernameFieldId,
passwordFieldId,
formId,
unchangedSelector,
} = data;
let form = content.document.querySelector(`[description="${formId}"]`);
await ContentTaskUtils.waitForEvent(
form,
"input",
"Username input value changed"
);
if (usernameFieldId) {
let usernameField = content.document.getElementById(usernameFieldId);
// If we have an username field, check if it's correctly filled
if (usernameField.getAttribute("expectedFail") == null) {
Assert.equal(
username,
usernameField.value,
"Username filled and correct."
);
}
}
if (passwordFieldId) {
let passwordField = content.document.getElementById(passwordFieldId);
// If we have a password field, check if it's correctly filled
if (passwordField && passwordField.getAttribute("expectedFail") == null) {
Assert.equal(
password,
passwordField.value,
"Password filled and correct."
);
}
}
let unchangedFields = content.document.querySelectorAll(unchangedSelector);
// Check that all fields that should not change have the same value as before.
if (unchangedFields.length) {
Assert.ok(() => {
for (let field of unchangedFields) {
if (field.value != field.getAttribute("original-value")) {
return false;
}
}
return true;
}, "Other fields were not changed.");
}
});
loginItem.doCommand();
return continuePromise;
}
/**
* Check if every login that matches the page origin are available at the context menu.
* @param {Element} contextMenu
* @param {Number} expectedCount - Number of logins expected in the context menu. Used to ensure
* we continue testing something useful.
*/
function checkMenu(contextMenu, expectedCount) {
let logins = loginList().filter(login => {
return LoginHelper.isOriginMatching(login.origin, TEST_ORIGIN, {
schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
});
});
// Make an array of menuitems for easier comparison.
let menuitems = [
...CONTEXT_MENU.getElementsByClassName("context-login-item"),
];
Assert.equal(
menuitems.length,
expectedCount,
"Expected number of menu items"
);
Assert.ok(
logins.every(l => menuitems.some(m => l.username == m.label)),
"Every login have an item at the menu."
);
}
/**
* Search for a login by it's username.
*
* Only unique login/origin combinations should be used at this test.
*/
function getLoginFromUsername(username) {
return loginList().find(login => login.username == username);
}
/**
* List of logins used for the test.
*
* We should only use unique usernames in this test,
* because we need to search logins by username. There is one duplicate u+p combo
* in order to test de-duping in the menu.
*/
function loginList() {
return [
LoginTestUtils.testData.formLogin({
origin: "https://example.com",
formActionOrigin: "https://example.com",
username: "username",
password: "password",
}),
// Same as above but HTTP in order to test de-duping.
LoginTestUtils.testData.formLogin({
origin: "http://example.com",
formActionOrigin: "http://example.com",
username: "username",
password: "password",
}),
LoginTestUtils.testData.formLogin({
origin: "http://example.com",
formActionOrigin: "http://example.com",
username: "username1",
password: "password1",
}),
LoginTestUtils.testData.formLogin({
origin: "https://example.com",
formActionOrigin: "https://example.com",
username: "username2",
password: "password2",
}),
LoginTestUtils.testData.formLogin({
origin: "http://example.org",
formActionOrigin: "http://example.org",
username: "username-cross-origin",
password: "password-cross-origin",
}),
];
}