зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
c251a9a6c5
Коммит
5899ac5cb2
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче