Bug 1550095 - Add Save Changes and Cancel button for LoginItem. r=MattN,Pike

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

--HG--
rename : browser/components/aboutlogins/tests/browser/browser_loginChanges.js => browser/components/aboutlogins/tests/browser/browser_loginListChanges.js
extra : moz-landing-system : lando
This commit is contained in:
Jared Wein 2019-05-10 21:29:51 +00:00
Родитель 50a50caf58
Коммит 03f122082c
13 изменённых файлов: 225 добавлений и 49 удалений

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

@ -36,6 +36,7 @@ let LEGACY_ACTORS = {
module: "resource:///actors/AboutLoginsChild.jsm",
events: {
"AboutLoginsDeleteLogin": {wantUntrusted: true},
"AboutLoginsUpdateLogin": {wantUntrusted: true},
"AboutLoginsInit": {wantUntrusted: true},
},
messages: [
@ -545,6 +546,7 @@ const listeners = {
mm: {
"AboutLogins:DeleteLogin": ["AboutLoginsParent"],
"AboutLogins:UpdateLogin": ["AboutLoginsParent"],
"AboutLogins:Subscribe": ["AboutLoginsParent"],
"Content:Click": ["ContentClick"],
"ContentSearch": ["ContentSearch"],

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

@ -30,6 +30,10 @@ class AboutLoginsChild extends ActorChild {
this.mm.sendAsyncMessage("AboutLogins:DeleteLogin", {login: event.detail});
break;
}
case "AboutLoginsUpdateLogin": {
this.mm.sendAsyncMessage("AboutLogins:UpdateLogin", {login: event.detail});
break;
}
}
}

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

@ -6,11 +6,16 @@
var EXPORTED_SYMBOLS = ["AboutLoginsParent"];
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineModuleGetter(this, "LoginHelper",
"resource://gre/modules/LoginHelper.jsm");
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "log", () => {
return LoginHelper.createLogger("AboutLoginsParent");
});
const ABOUT_LOGINS_ORIGIN = "about:logins";
const isValidLogin = login => {
@ -52,6 +57,25 @@ var AboutLoginsParent = {
messageManager.sendAsyncMessage("AboutLogins:AllLogins", this.getAllLogins());
break;
}
case "AboutLogins:UpdateLogin": {
let loginUpdates = message.data.login;
let logins = LoginHelper.searchLoginsWithObject({guid: loginUpdates.guid});
if (!logins || logins.length != 1) {
log.warn(`AboutLogins:UpdateLogin: expected to find a login for guid: ${loginUpdates.guid} but found ${(logins || []).length}`);
return;
}
let modifiedLogin = logins[0].clone();
if (loginUpdates.hasOwnProperty("username")) {
modifiedLogin.username = loginUpdates.username;
}
if (loginUpdates.hasOwnProperty("password")) {
modifiedLogin.password = loginUpdates.password;
}
Services.logins.modifyLogin(logins[0], modifiedLogin);
break;
}
}
},
@ -95,7 +119,8 @@ var AboutLoginsParent = {
messageSubscribers(name, details) {
let subscribers = ChromeUtils.nondeterministicGetWeakSetKeys(this._subscribers);
for (let subscriber of subscribers) {
if (subscriber.contentPrincipal.originNoSuffix != ABOUT_LOGINS_ORIGIN) {
if (!subscriber.contentPrincipal ||
subscriber.contentPrincipal.originNoSuffix != ABOUT_LOGINS_ORIGIN) {
this._subscribers.delete(subscriber);
continue;
}

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

@ -16,8 +16,12 @@ login-list =
.login-list-header = Logins
login-item =
.cancel-button = Cancel
.delete-button = Delete
.hostname-label = Hostname
.hostname-label = Website Address
.password-label = Password
.time-created-label = Time Created
.save-changes-button = Save Changes
.time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
.time-changed = Last changed: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
.time-used = Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
.username-label = Username

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

@ -18,10 +18,14 @@
<login-list data-l10n-id="login-list"
data-l10n-attrs="login-list-header"></login-list>
<login-item data-l10n-id="login-item"
data-l10n-attrs="delete-button,
data-l10n-attrs="cancel-button,
delete-button,
hostname-label,
password-label,
time-created-label,
save-changes-button,
time-created,
time-changed,
time-used,
username-label"></login-item>
<template id="login-list-template">
@ -43,23 +47,24 @@
<template id="login-item-template">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-item.css">
<h2 class="header"></h2>
<button class="delete-button"></button>
<label>
<span class="hostname-label"></span>
<input name="hostname"/>
<span class="hostname-label field-label"></span>
<span class="hostname"/>
</label>
<label>
<span class="username-label"></span>
<span class="username-label field-label"></span>
<input name="username"/>
</label>
<label>
<span class="password-label"></span>
<span class="password-label field-label"></span>
<input type="password" name="password"/>
</label>
<p>
<span class="time-created-label"></span>
<span class="time-created"></span>
</p>
<button class="delete-button"></button>
<p class="time-created meta-info"></p>
<p class="time-changed meta-info"></p>
<p class="time-used meta-info"></p>
<button class="save-changes-button"></button>
<button class="cancel-button"></button>
</template>
</body>
</html>

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

@ -19,7 +19,6 @@ window.addEventListener("AboutLoginsChromeToContent", event => {
}
case "LoginAdded": {
gElements.loginList.loginAdded(event.detail.value);
gElements.loginItem.loginAdded(event.detail.value);
break;
}
case "LoginModified": {

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

@ -9,3 +9,23 @@
h2 {
border-bottom: 1px solid var(--grey-30);
}
.field-label {
display: block;
}
.meta-info {
font-size: smaller;
}
.meta-info:not(:first-of-type) {
margin-top: 0;
}
.meta-info:not(:last-of-type) {
margin-bottom: 0;
}
.meta-info:first-of-type {
border-top: 1px solid var(--grey-30);
}

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

@ -18,8 +18,14 @@ class LoginItem extends HTMLElement {
this.attachShadow({mode: "open"})
.appendChild(loginItemTemplate.content.cloneNode(true));
let deleteButton = this.shadowRoot.querySelector(".delete-button");
deleteButton.addEventListener("click", this);
for (let selector of [
".delete-button",
".save-changes-button",
".cancel-button",
]) {
let button = this.shadowRoot.querySelector(selector);
button.addEventListener("click", this);
}
window.addEventListener("AboutLoginsLoginSelected", this);
@ -28,10 +34,14 @@ class LoginItem extends HTMLElement {
static get observedAttributes() {
return [
"cancel-button",
"delete-button",
"hostname-label",
"password-label",
"time-created-label",
"save-changes-button",
"time-created",
"time-changed",
"time-used",
"username-label",
];
}
@ -50,10 +60,17 @@ class LoginItem extends HTMLElement {
}
render() {
this.shadowRoot.querySelector("input[name='hostname']").value = this._login.hostname || "";
let l10nArgs = {
timeCreated: this._login.timeCreated || "",
timeChanged: this._login.timePasswordChanged || "",
timeUsed: this._login.timeLastUsed || "",
};
document.l10n.setAttributes(this, "login-item", l10nArgs);
let hostnameNoScheme = this._login.hostname && new URL(this._login.hostname).hostname;
this.shadowRoot.querySelector(".header").textContent = hostnameNoScheme || "";
this.shadowRoot.querySelector(".hostname").textContent = this._login.hostname || "";
this.shadowRoot.querySelector("input[name='username']").value = this._login.username || "";
this.shadowRoot.querySelector("input[name='password']").value = this._login.password || "";
this.shadowRoot.querySelector(".time-created").textContent = this._login.timeCreated || "";
}
handleEvent(event) {
@ -68,6 +85,28 @@ class LoginItem extends HTMLElement {
bubbles: true,
detail: this._login,
}));
return;
}
if (event.target.classList.contains("save-changes-button")) {
let loginUpdates = {
guid: this._login.guid,
};
let formUsername = this.shadowRoot.querySelector("input[name='username']").value.trim();
if (formUsername != this._login.username) {
loginUpdates.username = formUsername;
}
let formPassword = this.shadowRoot.querySelector("input[name='password']").value.trim();
if (formPassword != this._login.password) {
loginUpdates.password = formPassword;
}
document.dispatchEvent(new CustomEvent("AboutLoginsUpdateLogin", {
bubbles: true,
detail: loginUpdates,
}));
return;
}
if (event.target.classList.contains("cancel-button")) {
this.render();
}
break;
}
@ -79,27 +118,6 @@ class LoginItem extends HTMLElement {
this.render();
}
loginAdded(login) {
if (!this._login.guid) {
let tempLogin = {
username: this.shadowRoot.querySelector("input[name='username']").value,
formSubmitURL: "", // Use the wildcard since the user doesn't supply it.
hostname: this.shadowRoot.querySelector("input[name='hostname']").value,
password: this.shadowRoot.querySelector("input[name='password']").value,
};
// Need to use LoginHelper.doLoginsMatch() to see if the login
// that was added is the login that was being edited, so we
// can update time-created, etc.
if (window.AboutLoginsUtils.doLoginsMatch(tempLogin, login)) {
this._login = login;
this.render();
}
} else if (login.guid == this._login.guid) {
this._login = login;
this.render();
}
}
loginModified(login) {
if (login.guid != this._login.guid) {
return;

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

@ -3,4 +3,5 @@ prefs =
signon.management.page.enabled=true
[browser_deleteLogin.js]
[browser_loginChanges.js]
[browser_loginListChanges.js]
[browser_updateLogin.js]

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

@ -0,0 +1,66 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
let nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
Ci.nsILoginInfo, "init");
const LOGIN_URL = "https://www.example.com";
let TEST_LOGIN1 = new nsLoginInfo(LOGIN_URL, LOGIN_URL, null, "user1", "pass1", "username", "password");
add_task(async function setup() {
TEST_LOGIN1 = Services.logins.addLogin(TEST_LOGIN1);
await BrowserTestUtils.openNewForegroundTab({gBrowser, url: "about:logins"});
registerCleanupFunction(() => {
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
});
add_task(async function test_show_logins() {
let browser = gBrowser.selectedBrowser;
await ContentTask.spawn(browser, TEST_LOGIN1.guid, async (loginGuid) => {
let loginList = Cu.waiveXrays(content.document.querySelector("login-list"));
let loginFound = await ContentTaskUtils.waitForCondition(() => {
return loginList._logins.length == 1 &&
loginList._logins[0].guid == loginGuid;
}, "Waiting for login to be displayed");
ok(loginFound, "Stored logins should be displayed upon loading the page");
});
});
add_task(async function test_login_item() {
let browser = gBrowser.selectedBrowser;
await ContentTask.spawn(browser, LoginHelper.loginToVanillaObject(TEST_LOGIN1), async (login) => {
let loginList = content.document.querySelector("login-list");
let loginListItem = Cu.waiveXrays(loginList.shadowRoot.querySelector("login-list-item"));
loginListItem.click();
let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
let loginItemPopulated = await ContentTaskUtils.waitForCondition(() => {
return loginItem._login.guid == loginListItem.getAttribute("guid") &&
loginItem._login.guid == login.guid;
}, "Waiting for login item to get populated");
ok(loginItemPopulated, "The login item should get populated");
let usernameInput = loginItem.shadowRoot.querySelector("input[name='username']");
let passwordInput = loginItem.shadowRoot.querySelector("input[name='password']");
usernameInput.value += "-undome";
passwordInput.value += "-undome";
let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
cancelButton.click();
await Promise.resolve();
is(usernameInput.value, login.username, "Username change should be reverted");
is(passwordInput.value, login.password, "Password change should be reverted");
usernameInput.value += "-saveme";
passwordInput.value += "-saveme";
let saveChangesButton = loginItem.shadowRoot.querySelector(".save-changes-button");
saveChangesButton.click();
await ContentTaskUtils.waitForCondition(() => {
return loginListItem._login.username == usernameInput.value &&
loginListItem._login.password == passwordInput.value;
}, "Waiting for corresponding login in login list to update");
});
});

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

@ -1,6 +1,6 @@
"use strict";
/* exported asyncElementRendered, importDependencies */
/* exported asyncElementRendered, importDependencies, stubFluentL10n */
/**
* A helper to await on while waiting for an asynchronous rendering of a Custom
@ -24,3 +24,15 @@ function importDependencies(templateFrame, destinationEl) {
destinationEl.appendChild(imported);
}
}
function stubFluentL10n(argsMap) {
document.l10n = {
setAttributes(element, id, args) {
element.setAttribute("data-l10n-id", id);
for (let attrName of Object.keys(argsMap)) {
let varName = argsMap[attrName];
element.setAttribute(attrName, args[varName]);
}
},
};
}

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

@ -31,9 +31,17 @@ const TEST_LOGIN_1 = {
username: "user1",
password: "pass1",
timeCreated: "1000",
timePasswordChanged: "2000",
timeLastUsed: "4000",
};
add_task(async function setup() {
stubFluentL10n({
"time-created": "timeCreated",
"time-changed": "timeChanged",
"time-used": "timeUsed",
});
let templateFrame = document.getElementById("templateFrame");
let displayEl = document.getElementById("display");
importDependencies(templateFrame, displayEl);
@ -44,20 +52,24 @@ add_task(async function setup() {
add_task(async function test_empty_item() {
ok(gLoginItem, "loginItem exists");
is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, "", "hostname should be blank");
is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, "", "hostname should be blank");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be blank");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be blank");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank");
});
add_task(async function test_set_login() {
gLoginItem.setLogin(TEST_LOGIN_1);
await asyncElementRendered();
is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, TEST_LOGIN_1.hostname, "hostname should be populated");
is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be populated");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be populated");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
});
add_task(async function test_different_login_modified() {
@ -65,10 +77,12 @@ add_task(async function test_different_login_modified() {
gLoginItem.loginModified(otherLogin);
await asyncElementRendered();
is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, TEST_LOGIN_1.hostname, "hostname should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
});
add_task(async function test_different_login_removed() {
@ -76,10 +90,12 @@ add_task(async function test_different_login_removed() {
gLoginItem.loginRemoved(otherLogin);
await asyncElementRendered();
is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, TEST_LOGIN_1.hostname, "hostname should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, TEST_LOGIN_1.hostname, "hostname should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
});
add_task(async function test_login_modified() {
@ -87,20 +103,24 @@ add_task(async function test_login_modified() {
gLoginItem.loginModified(modifiedLogin);
await asyncElementRendered();
is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, modifiedLogin.hostname, "hostname should be updated");
is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, modifiedLogin.hostname, "hostname should be updated");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, modifiedLogin.username, "username should be updated");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, modifiedLogin.password, "password should be updated");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, modifiedLogin.timeCreated, "time-created should be updated");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, modifiedLogin.timePasswordChanged, "time-changed should be updated");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, modifiedLogin.timeLastUsed, "time-used should be updated");
});
add_task(async function test_login_removed() {
gLoginItem.loginRemoved(TEST_LOGIN_1);
await asyncElementRendered();
is(gLoginItem.shadowRoot.querySelector("input[name='hostname']").value, "", "hostname should be cleared");
is(gLoginItem.shadowRoot.querySelector(".hostname").textContent, "", "hostname should be cleared");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be cleared");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be cleared");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be cleared");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be cleared");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be cleared");
});
</script>