Bug 1200472 - Include subdomain login fill suggestions in the context menu. r=sfoster

Differential Revision: https://phabricator.services.mozilla.com/D51354

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Matthew Noorenberghe 2019-11-15 01:03:48 +00:00
Родитель c251a9a6c5
Коммит 5899ac5cb2
11 изменённых файлов: 531 добавлений и 222 удалений

Просмотреть файл

@ -974,10 +974,14 @@ class nsContextMenu {
}
let formOrigin = LoginHelper.getLoginOrigin(documentURI.spec);
let formActionOrigin = LoginHelper.getLoginOrigin(
loginFillInfo.formActionOrigin
);
let fragment = nsContextMenu.LoginManagerContextMenu.addLoginsToMenu(
this.targetIdentifier,
this.browser,
formOrigin
formOrigin,
formActionOrigin
);
let isGeneratedPasswordEnabled =
LoginHelper.generationAvailable && LoginHelper.generationEnabled;

Просмотреть файл

@ -462,6 +462,15 @@ notification[value="translation"] menulist > .menulist-dropmarker {
margin-inline-end: 0 !important;
}
#fill-login-popup > menucaption {
color: GrayText;
font-weight: normal;
}
#fill-login-popup > menucaption > .menu-iconic-left {
display: none;
}
.webextension-popup-browser,
.webextension-popup-stack {
border-radius: inherit;

Просмотреть файл

@ -818,6 +818,12 @@ menulist.translate-infobar-element > .menulist-dropmarker {
padding-right: 0;
}
#fill-login-popup > menucaption {
color: -moz-mac-menushadow;
font-weight: normal;
padding-inline-start: 11px;
}
.cui-widget-panelview[id^=PanelUI-webext-] {
border-radius: 3.5px;
}

Просмотреть файл

@ -840,6 +840,19 @@ panel[touchmode] .PanelUI-subView #appMenu-zoom-controls > .subviewbutton-iconic
margin-top: -4px;
}
#fill-login-popup > menucaption {
color: GrayText;
}
/* Match the treatment of menucaption used for <optgroup> */
#fill-login-popup > menucaption > .menu-iconic-left {
display: none
}
#fill-login-popup > menucaption > .menu-iconic-text {
-moz-appearance: menuitemtext
}
%include browser-aero.css
@media (-moz-os-version: windows-win7) {

Просмотреть файл

@ -65,6 +65,8 @@ XPCOMUtils.defineLazyGetter(this, "passwordMgrBundle", () => {
function loginSort(formHostPort, a, b) {
let maybeHostPortA = LoginHelper.maybeGetHostPortForURL(a.origin);
let maybeHostPortB = LoginHelper.maybeGetHostPortForURL(b.origin);
// Exact hostPort matches should appear first.
if (formHostPort == maybeHostPortA && formHostPort != maybeHostPortB) {
return -1;
}
@ -82,18 +84,8 @@ function loginSort(formHostPort, a, b) {
}
}
let userA = a.username.toLowerCase();
let userB = b.username.toLowerCase();
if (userA < userB) {
return -1;
}
if (userA > userB) {
return 1;
}
return 0;
// Finally sort by username.
return a.username.localeCompare(b.username);
}
function findDuplicates(loginList) {

Просмотреть файл

@ -2241,7 +2241,10 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
usernameField = aField;
}
let form = LoginFormFactory.createFromField(aField);
return {
formActionOrigin: LoginHelper.getFormActionOrigin(form),
usernameField: {
found: !!usernameField,
disabled:

Просмотреть файл

@ -40,10 +40,17 @@ this.LoginManagerContextMenu = {
* The origin of the document that the context menu was activated from.
* This isn't the same as the browser's top-level document origin
* when subframes are involved.
* @param {string} formActionOrigin
* The origin of the LoginForm's action.
* @returns {DocumentFragment} a document fragment with all the login items.
*/
addLoginsToMenu(inputElementIdentifier, browser, formOrigin) {
let foundLogins = this._findLogins(formOrigin);
addLoginsToMenu(
inputElementIdentifier,
browser,
formOrigin,
formActionOrigin
) {
let foundLogins = this._findLogins(formOrigin, formActionOrigin);
if (!foundLogins.length) {
return null;
@ -51,7 +58,32 @@ this.LoginManagerContextMenu = {
let fragment = browser.ownerDocument.createDocumentFragment();
let duplicateUsernames = this._findDuplicates(foundLogins);
// Default `lastDisplayOrigin` to the hostPort of the form so that we don't
// show a menucaption above logins that are direct matches for this document.
let lastDisplayOrigin = LoginHelper.maybeGetHostPortForURL(formOrigin);
let lastMenuCaption = null;
for (let login of foundLogins) {
// Add a section header containing the displayOrigin above logins that
// aren't matches for the form's origin.
if (lastDisplayOrigin != login.displayOrigin) {
if (fragment.children.length) {
let menuSeparator = fragment.ownerDocument.createXULElement(
"menuseparator"
);
menuSeparator.className = "context-login-item";
fragment.appendChild(menuSeparator);
}
lastMenuCaption = fragment.ownerDocument.createXULElement(
"menucaption"
);
lastMenuCaption.setAttribute("role", "group");
lastMenuCaption.label = login.displayOrigin;
lastMenuCaption.className = "context-login-item";
fragment.appendChild(lastMenuCaption);
}
let item = fragment.ownerDocument.createXULElement("menuitem");
let username = login.username;
@ -66,8 +98,16 @@ this.LoginManagerContextMenu = {
);
username = this._getLocalizedString("loginHostAge", [username, time]);
}
item.id = "login-" + login.guid;
item.setAttribute("label", username);
item.setAttribute("class", "context-login-item");
if (lastMenuCaption) {
item.setAttribute("aria-level", "2");
lastMenuCaption.setAttribute(
"aria-owns",
lastMenuCaption.getAttribute("aria-owns") + item.id + " "
);
}
// login is bound so we can keep the reference to each object.
item.addEventListener(
@ -83,6 +123,7 @@ this.LoginManagerContextMenu = {
);
fragment.appendChild(item);
lastDisplayOrigin = login.displayOrigin;
}
return fragment;
@ -140,51 +181,52 @@ this.LoginManagerContextMenu = {
});
},
loginSort(formHostPort, a, b) {
let maybeHostPortA = LoginHelper.maybeGetHostPortForURL(a.origin);
let maybeHostPortB = LoginHelper.maybeGetHostPortForURL(b.origin);
// Exact hostPort matches should appear first.
if (formHostPort == maybeHostPortA && formHostPort != maybeHostPortB) {
return -1;
}
if (formHostPort != maybeHostPortA && formHostPort == maybeHostPortB) {
return 1;
}
// Next sort by displayOrigin (which contains the httpRealm)
if (a.displayOrigin !== b.displayOrigin) {
return a.displayOrigin.localeCompare(b.displayOrigin);
}
// Finally sort by username within the displayOrigin.
return a.username.localeCompare(b.username);
},
/**
* Find logins for the specified origin..
* Find logins for the specified origin.
*
* @param {string} formOrigin
* Origin of the logins we want to find that has be sanitized by `getLoginOrigin`.
* This isn't the same as the browser's top-level document URI
* when subframes are involved.
* @param {string} formActionOrigin
*
* @returns {nsILoginInfo[]} a login list
*/
_findLogins(formOrigin) {
_findLogins(formOrigin, formActionOrigin) {
let searchParams = {
origin: formOrigin,
schemeUpgrades: LoginHelper.schemeUpgrades,
acceptDifferentSubdomains: LoginHelper.includeOtherSubdomainsInLookup,
formActionOrigin,
ignoreActionAndRealm: true,
};
let logins = LoginHelper.searchLoginsWithObject(searchParams);
let resolveBy = ["scheme", "timePasswordChanged"];
logins = LoginHelper.dedupeLogins(
logins,
["username", "password"],
resolveBy,
formOrigin
let logins = LoginManagerParent.searchAndDedupeLogins(
formOrigin,
searchParams
);
// Sort logins in alphabetical order and by date.
logins.sort((loginA, loginB) => {
// Sort alphabetically
let result = loginA.username.localeCompare(loginB.username);
if (result) {
// Forces empty logins to be at the end
if (!loginA.username) {
return 1;
}
if (!loginB.username) {
return -1;
}
return result;
}
// Same username logins are sorted by last change date
let metaA = loginA.QueryInterface(Ci.nsILoginMetaInfo);
let metaB = loginB.QueryInterface(Ci.nsILoginMetaInfo);
return metaB.timePasswordChanged - metaA.timePasswordChanged;
});
let formHostPort = LoginHelper.maybeGetHostPortForURL(formOrigin);
logins.sort(this.loginSort.bind(null, formHostPort));
return logins;
},

Просмотреть файл

@ -215,7 +215,7 @@ this.LoginTestUtils.testData = {
"form_field_password"
),
// Subdomains are treated as completely different sites.
// Subdomains can be treated as completely different sites depending on the UI invoked.
new LoginInfo(
"https://example.com",
"https://example.com",

Просмотреть файл

@ -299,7 +299,7 @@ add_task(async function fill_generated_password_with_matching_logins() {
let popupMenu = document.getElementById("fill-login-popup");
let firstLoginItem = popupMenu.getElementsByClassName(
"context-login-item"
)[0];
)[1];
firstLoginItem.doCommand();
await passwordChangedPromise;

Просмотреть файл

@ -174,7 +174,7 @@ add_task(async function test_searchAndDedupeLogins_acceptDifferentSubdomains() {
"Check length of added logins"
);
let actual = new LMP()._searchAndDedupeLogins(tc.formActionOrigin, {
let actual = LMP.searchAndDedupeLogins(tc.formActionOrigin, {
formActionOrigin: tc.formActionOrigin,
looseActionOriginMatch: true,
acceptDifferentSubdomains: true,

Просмотреть файл

@ -8,6 +8,67 @@ const { LoginManagerContextMenu } = ChromeUtils.import(
"resource://gre/modules/LoginManagerContextMenu.jsm"
);
const dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
dateStyle: "medium",
});
const ORIGIN_HTTP_EXAMPLE_ORG = "http://example.org";
const ORIGIN_HTTPS_EXAMPLE_ORG = "https://example.org";
const ORIGIN_HTTPS_EXAMPLE_ORG_8080 = "https://example.org:8080";
const ORIGIN_HTTPS_SUB_EXAMPLE_ORG = "https://sub.example.org";
const FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1 = formLogin({
formActionOrigin: ORIGIN_HTTPS_EXAMPLE_ORG,
guid: "FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1",
origin: ORIGIN_HTTPS_EXAMPLE_ORG,
});
// HTTP version of the above
const FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1 = formLogin({
formActionOrigin: ORIGIN_HTTP_EXAMPLE_ORG,
guid: "FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1",
origin: ORIGIN_HTTP_EXAMPLE_ORG,
});
// Same as above but with a different password
const FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P2 = formLogin({
formActionOrigin: ORIGIN_HTTP_EXAMPLE_ORG,
guid: "FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P2",
origin: ORIGIN_HTTP_EXAMPLE_ORG,
password: "pass2",
});
// Non-default port
const FORM_LOGIN_HTTPS_EXAMPLE_ORG_8080_U1_P2 = formLogin({
formActionOrigin: ORIGIN_HTTPS_EXAMPLE_ORG_8080,
guid: "FORM_LOGIN_HTTPS_EXAMPLE_ORG_8080_U1_P2",
origin: ORIGIN_HTTPS_EXAMPLE_ORG_8080,
password: "pass2",
});
// Subdomain
const FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P1 = formLogin({
formActionOrigin: ORIGIN_HTTPS_SUB_EXAMPLE_ORG,
guid: "FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P1",
origin: ORIGIN_HTTPS_SUB_EXAMPLE_ORG,
});
const FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P2 = formLogin({
formActionOrigin: ORIGIN_HTTPS_SUB_EXAMPLE_ORG,
guid: "FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P2",
origin: ORIGIN_HTTPS_SUB_EXAMPLE_ORG,
password: "pass2",
});
// HTTP Auth.
const HTTP_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1 = authLogin({
guid: "FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1",
origin: ORIGIN_HTTPS_EXAMPLE_ORG,
});
XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
return Services.strings.createBundle(
"chrome://passwordmgr/locale/passwordmgr.properties"
@ -18,69 +79,337 @@ XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
* Prepare data for the following tests.
*/
add_task(async function test_initialize() {
for (let login of loginList()) {
Services.logins.addLogin(login);
}
Services.prefs.setBoolPref("signon.schemeUpgrades", true);
Services.prefs.setBoolPref("signon.includeOtherSubdomainsInLookup", true);
});
add_task(async function test_sameOriginOnlyHTTPS() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1],
expectedItems: [
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
},
],
});
});
add_task(async function test_sameOriginOnlyHTTPS_noUsername() {
let loginWithoutUsername = FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.clone();
loginWithoutUsername.QueryInterface(Ci.nsILoginMetaInfo).guid = "no-username";
loginWithoutUsername.username = "";
await runTestcase({
formOrigin: loginWithoutUsername.origin,
savedLogins: [loginWithoutUsername],
expectedItems: [
{
login: loginWithoutUsername,
time: true,
},
],
});
});
add_task(async function test_sameOriginOnlyHTTP() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1],
expectedItems: [
{
login: FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1,
},
],
});
});
// Scheme upgrade/downgrade tasks
add_task(async function test_sameOriginDedupeSchemeUpgrade() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1,
],
expectedItems: [
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
},
],
});
});
add_task(async function test_sameOriginSchemeDowngrade() {
// Should have no https: when formOrigin is https:
await runTestcase({
formOrigin: FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1,
],
expectedItems: [
{
login: FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1,
},
],
});
});
add_task(async function test_sameOriginShadowedSchemeUpgrade() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P2, // Different password
],
expectedItems: [
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
},
],
});
});
add_task(async function test_sameOriginShadowedSchemeDowngrade() {
// Should have no https: when formOrigin is https:
await runTestcase({
formOrigin: FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P2, // Different password
],
expectedItems: [
{
login: FORM_LOGIN_HTTP_EXAMPLE_ORG_U1_P2,
},
],
});
});
// Non-default port tasks
add_task(async function test_sameDomainDifferentPort_onDefault() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTPS_EXAMPLE_ORG_8080_U1_P2,
],
expectedItems: [
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
time: true,
},
"--", // separator
FORM_LOGIN_HTTPS_EXAMPLE_ORG_8080_U1_P2.displayOrigin, // group heading
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_8080_U1_P2,
time: true,
},
],
});
});
add_task(async function test_sameDomainDifferentPort_onNonDefault() {
await runTestcase({
// Swap the formOrigin compared to above
formOrigin: FORM_LOGIN_HTTPS_EXAMPLE_ORG_8080_U1_P2.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTPS_EXAMPLE_ORG_8080_U1_P2,
],
expectedItems: [
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_8080_U1_P2,
time: true,
},
"--", // separator
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.displayOrigin, // group heading
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
time: true,
},
],
});
});
// Subdomain tasks
add_task(async function test_onlySubdomainOnBaseDomain() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P1],
expectedItems: [
// No separator
FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P1.displayOrigin,
{
login: FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P1,
},
],
});
});
add_task(async function test_subdomainDedupeOnBaseDomain() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P1,
],
expectedItems: [
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
},
],
});
});
add_task(async function test_subdomainDedupeOnSubDomain() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P1,
],
expectedItems: [
{
login: FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P1,
},
],
});
});
add_task(async function test_subdomainIncludedOnBaseDomain() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P2,
],
expectedItems: [
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
time: true,
},
"--", // separator
FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P2.displayOrigin, // group heading
{
login: FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P2,
time: true,
},
],
});
});
add_task(async function test_subdomainIncludedOnSubDomain() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P2.origin,
savedLogins: [
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P2,
],
expectedItems: [
{
login: FORM_LOGIN_HTTPS_SUB_EXAMPLE_ORG_U1_P2,
time: true,
},
"--", // separator
FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.displayOrigin, // group heading
{
login: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
time: true,
},
],
});
});
// HTTP auth. suggestions
add_task(async function test_sameOriginOnlyHTTPAuth() {
await runTestcase({
formOrigin: FORM_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.origin,
savedLogins: [HTTP_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1],
expectedItems: [
// No separator
HTTP_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1.displayOrigin, // group heading
{
login: HTTP_LOGIN_HTTPS_EXAMPLE_ORG_U1_P1,
},
],
});
});
// Helpers
function formLogin(modifications = {}) {
let mods = Object.assign(
{},
{
timePasswordChanged: 1573821296000,
},
modifications
);
return TestData.formLogin(mods);
}
function authLogin(modifications = {}) {
let mods = Object.assign(
{},
{
timePasswordChanged: 1573821296000,
},
modifications
);
return TestData.authLogin(mods);
}
/**
* Tests if the LoginManagerContextMenu returns the correct login items.
*/
add_task(async function test_contextMenuAddAndRemoveLogins() {
async function runTestcase({ formOrigin, savedLogins, expectedItems }) {
const DOCUMENT_CONTENT = "<form><input id='pw' type=password></form>";
const INPUT_QUERY = "input[type='password']";
let testOrigins = [
"http://www.example.com",
"http://www2.example.com",
"http://www3.example.com",
"http://empty.example.com",
];
for (let origin of testOrigins) {
info("test for origin: " + origin);
// Get expected logins for this test.
let logins = getExpectedLogins(origin);
// Create the logins menuitems fragment.
let { fragment, document } = createLoginsFragment(
origin,
DOCUMENT_CONTENT,
INPUT_QUERY
);
if (!logins.length) {
Assert.ok(fragment === null, "Null returned. No logins where found.");
continue;
}
let items = [...fragment.querySelectorAll("menuitem")];
// Check if the items are those expected to be listed.
Assert.ok(checkLoginItems(logins, items), "All expected logins found.");
document.body.appendChild(fragment);
// Try to clear the fragment.
LoginManagerContextMenu.clearLoginsFromMenu(document);
Assert.equal(
fragment.querySelectorAll("menuitem").length,
0,
"All items correctly cleared."
);
for (let login of savedLogins) {
Services.logins.addLogin(login);
}
// Create the logins menuitems fragment.
let { fragment, document } = createLoginsFragment(
formOrigin,
DOCUMENT_CONTENT
);
if (!expectedItems.length) {
Assert.ok(fragment === null, "Null returned. No logins were found.");
return;
}
let actualItems = [...fragment.children];
// Check if the items are those expected to be listed.
checkLoginItems(actualItems, expectedItems);
document.body.appendChild(fragment);
// Try to clear the fragment.
LoginManagerContextMenu.clearLoginsFromMenu(document);
Assert.equal(
document.querySelectorAll("menuitem, menuseparator, menucaption").length,
0,
"All items correctly cleared."
);
Services.logins.removeAllLogins();
});
}
/**
* Create a fragment with a menuitem for each login.
*/
function createLoginsFragment(url, content, elementQuery) {
const CHROME_URL = "chrome://mock-chrome";
function createLoginsFragment(url, content) {
const CHROME_URL = "chrome://mock-chrome/content/";
// Create a mock document.
let document = MockDocument.createTestDocument(CHROME_URL, content);
let inputElement = document.querySelector(elementQuery);
MockDocument.mockOwnerDocumentProperty(inputElement, document, url);
// We also need a simple mock Browser object for this test.
document.createXULElement = document.createElement.bind(document);
@ -92,143 +421,54 @@ function createLoginsFragment(url, content, elementQuery) {
return {
document,
fragment: LoginManagerContextMenu.addLoginsToMenu(
inputElement,
null,
browser,
formOrigin
),
};
}
/**
* Check if every login have it's corresponding menuitem.
* Duplicates and empty usernames have a date appended.
*/
function checkLoginItems(logins, items) {
function findDuplicates(unfilteredLoginList) {
let seen = new Set();
let duplicates = new Set();
for (let login of unfilteredLoginList) {
if (seen.has(login.username)) {
duplicates.add(login.username);
}
seen.add(login.username);
}
return duplicates;
}
let duplicates = findDuplicates(logins);
function checkLoginItems(actualItems, expectedDetails) {
for (let [i, expectedDetail] of expectedDetails.entries()) {
let actualElement = actualItems[i];
let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
dateStyle: "medium",
});
for (let login of logins) {
if (login.username && !duplicates.has(login.username)) {
// If login is not duplicate and we can't find an item for it, fail.
if (!items.find(item => item.label == login.username)) {
return false;
}
// Separator
if (expectedDetail == "--") {
Assert.equal(actualElement.localName, "menuseparator", "Check localName");
continue;
}
let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
let time = dateAndTimeFormatter.format(new Date(meta.timePasswordChanged));
// If login is duplicate, check if we have a login item with appended date.
if (
login.username &&
!items.find(item => item.label == login.username + " (" + time + ")")
) {
return false;
// Section heading
if (typeof expectedDetail == "string") {
Assert.equal(actualElement.localName, "menucaption", "Check localName");
continue;
}
// If login is empty, check if we have a login item with appended date.
if (
!login.username &&
!items.find(
item =>
item.label ==
_stringBundle.GetStringFromName("noUsername") + " (" + time + ")"
)
) {
return false;
Assert.equal(actualElement.localName, "menuitem", "Check localName");
Assert.equal(
actualElement.id,
"login-" + expectedDetail.login.guid,
`Check id ${i}`
);
let expectedLabel = expectedDetail.login.username;
if (!expectedLabel) {
expectedLabel += _stringBundle.GetStringFromName("noUsername");
}
if (expectedDetail.time) {
expectedLabel +=
" (" +
dateAndTimeFormatter.format(
new Date(expectedDetail.login.timePasswordChanged)
) +
")";
}
Assert.equal(actualElement.label, expectedLabel, `Check label ${i}`);
}
return true;
}
/**
* Gets the list of expected logins for an origin.
*/
function getExpectedLogins(origin) {
return Services.logins
.getAllLogins()
.filter(entry => entry.origin === origin);
}
function loginList() {
return [
new LoginInfo(
"http://www.example.com",
"http://www.example.com",
null,
"username1",
"password",
"form_field_username",
"form_field_password"
),
new LoginInfo(
"http://www.example.com",
"http://www.example.com",
null,
"username2",
"password",
"form_field_username",
"form_field_password"
),
new LoginInfo(
"http://www2.example.com",
"http://www.example.com",
null,
"username",
"password",
"form_field_username",
"form_field_password"
),
new LoginInfo(
"http://www2.example.com",
"http://www2.example.com",
null,
"username",
"password2",
"form_field_username",
"form_field_password"
),
new LoginInfo(
"http://www2.example.com",
"http://www2.example.com",
null,
"username2",
"password2",
"form_field_username",
"form_field_password"
),
new LoginInfo(
"http://www3.example.com",
"http://www.example.com",
null,
"",
"password",
"form_field_username",
"form_field_password"
),
new LoginInfo(
"http://www3.example.com",
"http://www3.example.com",
null,
"",
"password2",
"form_field_username",
"form_field_password"
),
];
Assert.equal(
actualItems.length,
expectedDetails.length,
"Should have the correct number of menu items"
);
}