Bug 1553209 - Merge the modal-input custom element into the login-item custom element. r=MattN,fluent-reviewers,flod

The separation didn't work out right and some work started getting duplicated between login-item and modal-input.

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Jared Wein 2019-06-14 13:52:53 +00:00
parent f81c678374
commit d51be76b63
13 changed files with 192 additions and 453 deletions

View File

@ -38,13 +38,13 @@ login-item =
.copy-username-button = Copy
.delete-button = Delete
.edit-button = Edit
.modal-input-reveal-checkbox-hide = Hide password
.modal-input-reveal-checkbox-show = Show password
.new-login-title = Create New Login
.open-site-button = Launch
.origin-label = Website Address
.origin-placeholder = https://www.example.com
.password-hide-title = Hide password
.password-label = Password
.password-show-title = Show password
.save-changes-button = Save Changes
.time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
.time-changed = Last modified: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }

View File

@ -15,7 +15,6 @@
<script type="module" src="chrome://browser/content/aboutlogins/components/login-list.js"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/login-list-item.js"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/menu-button.js"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/modal-input.js"></script>
<script type="module" src="chrome://browser/content/aboutlogins/aboutLogins.js"></script>
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/aboutLogins.css">
@ -48,13 +47,13 @@
copied-username-button,
delete-button,
edit-button,
modal-input-reveal-checkbox-hide,
modal-input-reveal-checkbox-show,
new-login-title,
open-site-button,
origin-label,
origin-placeholder,
password-hide-title,
password-label,
password-show-title,
save-changes-button,
time-created,
time-changed,
@ -97,25 +96,27 @@
<button class="delete-button alternate-button"></button>
</div>
<div class="detail-row">
<label>
<label class="detail-cell">
<span class="origin-label field-label"></span>
<span class="origin-saved-value"></span>
<input type="url" name="origin" required/>
</label>
<button class="open-site-button"></button>
</div>
<div class="detail-row">
<label>
<label class="detail-cell">
<span class="username-label field-label"></span>
<modal-input name="username"/>
<input type="text" name="username"/>
</label>
<copy-to-clipboard-button class="copy-username-button"
data-telemetry-object="username"></copy-to-clipboard-button>
</div>
<div class="detail-row">
<label>
<label class="detail-cell">
<span class="password-label field-label"></span>
<modal-input type="password" name="password" required />
<div class="reveal-password-wrapper">
<input type="password" name="password" required/>
<input type="checkbox" class="reveal-password-checkbox"/>
</div>
</label>
<copy-to-clipboard-button class="copy-password-button"
data-telemetry-object="password"></copy-to-clipboard-button>
@ -143,14 +144,6 @@
</ul>
</template>
<template id="modal-input-template">
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/modal-input.css">
<span class="locked-value"></span>
<input type="text" class="unlocked-value"/>
<input type="checkbox" class="reveal-checkbox"/>
</template>
<template id="copy-to-clipboard-button-template">
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/copy-to-clipboard-button.css">

View File

@ -4,6 +4,10 @@
:host {
padding: 18px;
--reveal-checkbox-opacity: .8;
--reveal-checkbox-opacity-hover: .6;
--reveal-checkbox-opacity-active: 1;
}
:host([editing]) .edit-button,
@ -12,12 +16,20 @@
:host([isNewLogin]) copy-to-clipboard-button,
:host([isNewLogin]) .open-site-button,
:host([isNewLogin]) .meta-info,
:host(:not([isNewLogin])) input[name="origin"],
:host(:not([editing])) .save-changes-button,
:host(:not([editing])) .cancel-button {
:host(:not([editing])) .cancel-button,
:host(:not([editing])) .save-changes-button {
display: none;
}
:host(:not([editing])) input[type="password"],
:host(:not([editing])) input[type="text"],
:host(:not([editing])) input[type="url"] {
all: unset;
display: inline-block;
width: -moz-available;
background-color: transparent !important; /* override common.inc.css */
}
.header {
display: flex;
border-bottom: 1px solid var(--in-content-box-border-color);
@ -53,12 +65,17 @@
padding-inline-start: 32px; /* 8px on each side, and 16px for icon width */
}
.detail-row {
.detail-row,
.reveal-password-wrapper {
display: flex;
align-items: center;
}
.detail-row {
margin-bottom: 20px;
}
.detail-row > label {
.detail-cell {
flex: auto;
}
@ -84,3 +101,37 @@
/* Show a non-full length border but constrain the text to this width */
white-space: nowrap;
}
.reveal-password-checkbox {
/* !important is needed to override common.css styling for checkboxes */
background-color: transparent !important;
border-width: 0 !important;
background-image: url("chrome://browser/content/aboutlogins/icons/show-password.svg") !important;
margin-inline-start: 8px !important;
cursor: pointer;
-moz-context-properties: fill;
fill: currentColor !important;
opacity: var(--reveal-checkbox-opacity);
}
.reveal-password-checkbox:hover {
opacity: var(--reveal-checkbox-opacity-hover);
}
.reveal-password-checkbox:hover:active {
opacity: var(--reveal-checkbox-opacity-active);
}
.reveal-password-checkbox:checked {
background-image: url("chrome://browser/content/aboutlogins/icons/hide-password.svg") !important;
}
@supports -moz-bool-pref("browser.in-content.dark-mode") {
@media (prefers-color-scheme: dark) {
:host {
--reveal-checkbox-opacity: .8;
--reveal-checkbox-opacity-hover: 1;
--reveal-checkbox-opacity-active: .6;
}
}
}

View File

@ -39,10 +39,13 @@ export default class LoginItem extends ReflectedFluentElement {
let originInput = this.shadowRoot.querySelector("input[name='origin']");
originInput.addEventListener("blur", this);
let revealCheckbox = this.shadowRoot.querySelector(".reveal-password-checkbox");
revealCheckbox.addEventListener("click", this);
let copyUsernameButton = this.shadowRoot.querySelector(".copy-username-button");
let copyPasswordButton = this.shadowRoot.querySelector(".copy-password-button");
copyUsernameButton.relatedInput = this.shadowRoot.querySelector("modal-input[name='username']");
copyPasswordButton.relatedInput = this.shadowRoot.querySelector("modal-input[name='password']");
copyUsernameButton.relatedInput = this.shadowRoot.querySelector("input[name='username']");
copyPasswordButton.relatedInput = this.shadowRoot.querySelector("input[name='password']");
this.render();
}
@ -56,13 +59,13 @@ export default class LoginItem extends ReflectedFluentElement {
"copy-username-button",
"delete-button",
"edit-button",
"modal-input-reveal-checkbox-hide",
"modal-input-reveal-checkbox-show",
"new-login-title",
"open-site-button",
"origin-label",
"origin-placeholder",
"password-hide-title",
"password-label",
"password-show-title",
"save-changes-button",
"time-created",
"time-changed",
@ -92,16 +95,6 @@ export default class LoginItem extends ReflectedFluentElement {
copyUsernameButton.setAttribute(newAttrName, this.getAttribute(attrName));
break;
}
case "modal-input-reveal-checkbox-hide": {
this.shadowRoot.querySelector("modal-input[name='password']")
.setAttribute("reveal-checkbox-hide", this.getAttribute(attrName));
break;
}
case "modal-input-reveal-checkbox-show": {
this.shadowRoot.querySelector("modal-input[name='password']")
.setAttribute("reveal-checkbox-show", this.getAttribute(attrName));
break;
}
case "new-login-title": {
let title = this.shadowRoot.querySelector(".title");
title.setAttribute(attrName, this.getAttribute(attrName));
@ -115,8 +108,13 @@ export default class LoginItem extends ReflectedFluentElement {
originInput.setAttribute("placeholder", this.getAttribute(attrName));
break;
}
case "password-hide-title":
case "password-show-title": {
this.updatePasswordRevealState();
break;
}
case "username-placeholder": {
let usernameInput = this.shadowRoot.querySelector("modal-input[name='username']");
let usernameInput = this.shadowRoot.querySelector("input[name='username']");
usernameInput.setAttribute("placeholder", this.getAttribute(attrName));
break;
}
@ -136,10 +134,10 @@ export default class LoginItem extends ReflectedFluentElement {
let title = this.shadowRoot.querySelector(".title");
title.textContent = this._login.title || title.getAttribute("new-login-title");
this.shadowRoot.querySelector(".origin-saved-value").textContent = this._login.origin || "";
this.shadowRoot.querySelector("input[name='origin']").defaultValue = this._login.origin || "";
this.shadowRoot.querySelector("modal-input[name='username']").setAttribute("value", this._login.username || "");
this.shadowRoot.querySelector("modal-input[name='password']").setAttribute("value", this._login.password || "");
this.shadowRoot.querySelector("input[name='username']").defaultValue = this._login.username || "";
this.shadowRoot.querySelector("input[name='password']").defaultValue = this._login.password || "";
this.updatePasswordRevealState();
}
handleEvent(event) {
@ -162,8 +160,14 @@ export default class LoginItem extends ReflectedFluentElement {
}
case "click": {
if (event.target.classList.contains("cancel-button")) {
this.toggleEditing();
this.render();
if (this._login.guid) {
this.setLogin(this._login);
} else {
// TODO, should select the first login if it exists
// or show the no-logins view otherwise
this.toggleEditing();
this.render();
}
recordTelemetryEvent({
object: this._login.guid ? "existing_login" : "new_login",
@ -195,6 +199,14 @@ export default class LoginItem extends ReflectedFluentElement {
recordTelemetryEvent({object: "existing_login", method: "open_site"});
return;
}
if (event.target.classList.contains("reveal-password-checkbox")) {
this.updatePasswordRevealState();
let revealCheckbox = this.shadowRoot.querySelector(".reveal-password-checkbox");
let method = revealCheckbox.checked ? "show" : "hide";
recordTelemetryEvent({object: "password", method});
return;
}
if (event.target.classList.contains("save-changes-button")) {
if (!this._isFormValid({reportErrors: true})) {
return;
@ -228,9 +240,22 @@ export default class LoginItem extends ReflectedFluentElement {
let originInput =
this.resetValidation(this.shadowRoot.querySelector("input[name='origin']"), login.origin);
originInput.addEventListener("blur", this);
let usernameInput =
this.resetValidation(this.shadowRoot.querySelector("input[name='username']"), login.username);
let passwordInput =
this.resetValidation(this.shadowRoot.querySelector("input[name='password']"), login.password);
let copyUsernameButton = this.shadowRoot.querySelector(".copy-username-button");
let copyPasswordButton = this.shadowRoot.querySelector(".copy-password-button");
copyUsernameButton.relatedInput = usernameInput;
copyPasswordButton.relatedInput = passwordInput;
this.toggleAttribute("isNewLogin", !login.guid);
this.toggleEditing(!login.guid);
let revealCheckbox = this.shadowRoot.querySelector(".reveal-password-checkbox");
revealCheckbox.checked = false;
this.render();
}
@ -272,27 +297,55 @@ export default class LoginItem extends ReflectedFluentElement {
this.removeAttribute("isNewLogin");
}
if (shouldEdit) {
this.shadowRoot.querySelector("input[name='password']").style.removeProperty("width");
} else {
// Need to set a shorter width than -moz-available so the reveal checkbox
// will still appear next to the password.
this.shadowRoot.querySelector("input[name='password']").style.width =
(this._login.password || "").length + "ch";
}
this.shadowRoot.querySelector(".delete-button").disabled = this.hasAttribute("isNewLogin");
this.shadowRoot.querySelector(".edit-button").disabled = shouldEdit;
this.shadowRoot.querySelectorAll("modal-input")
.forEach(el => el.toggleAttribute("editing", shouldEdit));
this.shadowRoot.querySelector("input[name='origin']").readOnly = !this.hasAttribute("isNewLogin");
this.shadowRoot.querySelector("input[name='username']").readOnly = !shouldEdit;
this.shadowRoot.querySelector("input[name='password']").readOnly = !shouldEdit;
this.toggleAttribute("editing", shouldEdit);
}
resetValidation(formElement, value) {
let wasRequired = formElement.hasAttribute("required");
let wasRequired = formElement.required;
let newFormElement = document.createElement(formElement.localName);
newFormElement.defaultValue = value || "";
if (value) {
newFormElement.defaultValue = value;
}
newFormElement.className = formElement.className;
newFormElement.placeholder = formElement.placeholder;
newFormElement.setAttribute("name", formElement.getAttribute("name"));
newFormElement.setAttribute("type", formElement.getAttribute("type"));
if (wasRequired) {
newFormElement.setAttribute("required", "");
newFormElement.required = true;
}
formElement.replaceWith(newFormElement);
return newFormElement;
}
updatePasswordRevealState() {
let revealCheckbox = this.shadowRoot.querySelector(".reveal-password-checkbox");
let labelAttr = revealCheckbox.checked ? "password-show-title"
: "password-hide-title";
revealCheckbox.setAttribute("aria-label", this.getAttribute(labelAttr));
revealCheckbox.setAttribute("title", this.getAttribute(labelAttr));
let passwordInput = this.shadowRoot.querySelector("input[name='password']");
if (revealCheckbox.checked) {
passwordInput.setAttribute("type", "text");
return;
}
passwordInput.setAttribute("type", "password");
}
/**
* Checks that the edit/new-login form has valid values present for their
* respective required fields.
@ -301,7 +354,7 @@ export default class LoginItem extends ReflectedFluentElement {
* to the user.
*/
_isFormValid({reportErrors} = {}) {
let fields = [this.shadowRoot.querySelector("modal-input[name='password']")];
let fields = [this.shadowRoot.querySelector("input[name='password']")];
if (this.hasAttribute("isNewLogin")) {
fields.push(this.shadowRoot.querySelector("input[name='origin']"));
}
@ -320,10 +373,9 @@ export default class LoginItem extends ReflectedFluentElement {
_loginFromForm() {
return {
username: this.shadowRoot.querySelector("modal-input[name='username']").value.trim(),
password: this.shadowRoot.querySelector("modal-input[name='password']").value.trim(),
origin: this.hasAttribute("isNewLogin") ? this.shadowRoot.querySelector("input[name='origin']").value.trim()
: this.shadowRoot.querySelector(".origin-saved-value").textContent,
username: this.shadowRoot.querySelector("input[name='username']").value.trim(),
password: this.shadowRoot.querySelector("input[name='password']").value.trim(),
origin: this.shadowRoot.querySelector("input[name='origin']").value.trim(),
};
}
}

View File

@ -1,55 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
:host {
--reveal-button-opacity: .8;
--reveal-button-opacity-hover: .6;
--reveal-button-opacity-active: 1;
display: flex;
align-items: center;
}
:host([editing]) .locked-value,
:host(:not([editing])) .unlocked-value {
display: none;
}
:host(:not([type="password"])) .reveal-checkbox {
display: none;
}
.reveal-checkbox {
/* !important is needed to override common.css styling for checkboxes */
background-color: transparent !important;
border-width: 0 !important;
background-image: url("chrome://browser/content/aboutlogins/icons/show-password.svg") !important;
margin-inline-start: 8px !important;
cursor: pointer;
-moz-context-properties: fill;
fill: currentColor !important;
opacity: var(--reveal-button-opacity);
}
.reveal-checkbox:hover {
opacity: var(--reveal-button-opacity-hover);
}
.reveal-checkbox:hover:active {
opacity: var(--reveal-button-opacity-active);
}
.reveal-checkbox:checked {
background-image: url("chrome://browser/content/aboutlogins/icons/hide-password.svg") !important;
}
@supports -moz-bool-pref("browser.in-content.dark-mode") {
@media (prefers-color-scheme: dark) {
:host {
--reveal-button-opacity: .8;
--reveal-button-opacity-hover: 1;
--reveal-button-opacity-active: .6;
}
}
}

View File

@ -1,187 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {recordTelemetryEvent} from "chrome://browser/content/aboutlogins/aboutLoginsUtils.js";
import ReflectedFluentElement from "chrome://browser/content/aboutlogins/components/reflected-fluent-element.js";
export default class ModalInput extends ReflectedFluentElement {
static get LOCKED_PASSWORD_DISPLAY() {
return "••••••••";
}
connectedCallback() {
if (this.shadowRoot) {
return;
}
let modalInputTemplate = document.querySelector("#modal-input-template");
this.attachShadow({mode: "open"})
.appendChild(modalInputTemplate.content.cloneNode(true));
if (this.hasAttribute("value")) {
this.value = this.getAttribute("value");
}
let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
if (this.getAttribute("type") == "password") {
unlockedValue.setAttribute("type", "password");
}
if (this.getAttribute("placeholder")) {
unlockedValue.setAttribute("placeholder", this.getAttribute("placeholder"));
}
if (this.hasAttribute("required")) {
unlockedValue.setAttribute("required", "");
}
this.shadowRoot.querySelector(".reveal-checkbox").addEventListener("click", this);
}
static get reflectedFluentIDs() {
return ["reveal-checkbox-hide", "reveal-checkbox-show"];
}
static get observedAttributes() {
return [
"editing",
"placeholder",
"required",
"type",
"value",
].concat(ModalInput.reflectedFluentIDs);
}
handleSpecialCaseFluentString(attrName) {
if (attrName != "reveal-checkbox-hide" &&
attrName != "reveal-checkbox-show") {
return false;
}
this.updateRevealCheckboxTitle();
return true;
}
attributeChangedCallback(attr, oldValue, newValue) {
super.attributeChangedCallback(attr, oldValue, newValue);
if (!this.shadowRoot) {
return;
}
let lockedValue = this.shadowRoot.querySelector(".locked-value");
let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
switch (attr) {
case "editing": {
let isEditing = newValue !== null;
if (!isEditing) {
this.setAttribute("value", unlockedValue.defaultValue);
}
break;
}
case "placeholder": {
unlockedValue.setAttribute("placeholder", newValue);
break;
}
case "required": {
unlockedValue.toggleAttribute("required", this.hasAttribute("required"));
break;
}
case "type": {
if (newValue == "password") {
lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
unlockedValue.setAttribute("type", "password");
} else {
lockedValue.textContent = this.getAttribute("value");
unlockedValue.setAttribute("type", "text");
}
break;
}
case "value": {
this.value = newValue;
break;
}
}
}
handleEvent(event) {
switch (event.type) {
case "click": {
let revealCheckbox = event.target;
let lockedValue = this.shadowRoot.querySelector(".locked-value");
let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
if (revealCheckbox.checked) {
lockedValue.textContent = this.value;
unlockedValue.setAttribute("type", "text");
recordTelemetryEvent({object: "password", method: "show"});
} else {
lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
unlockedValue.setAttribute("type", "password");
recordTelemetryEvent({object: "password", method: "hide"});
}
this.updateRevealCheckboxTitle();
break;
}
}
}
get value() {
return this.hasAttribute("editing") ? this.shadowRoot.querySelector(".unlocked-value").value.trim()
: this.getAttribute("value") || "";
}
set value(val) {
if (this.getAttribute("value") != val) {
this.setAttribute("value", val);
return;
}
let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
let wasRequired = unlockedValue.hasAttribute("required");
let newUnlockedValue = document.createElement(unlockedValue.localName);
newUnlockedValue.defaultValue = val || "";
newUnlockedValue.className = unlockedValue.className;
newUnlockedValue.setAttribute("type", unlockedValue.getAttribute("type"));
if (wasRequired) {
newUnlockedValue.setAttribute("required", "");
}
unlockedValue.replaceWith(newUnlockedValue);
unlockedValue = newUnlockedValue;
let lockedValue = this.shadowRoot.querySelector(".locked-value");
if (this.getAttribute("type") == "password" && val && val.length) {
lockedValue.textContent = this.constructor.LOCKED_PASSWORD_DISPLAY;
} else {
lockedValue.textContent = val || "";
}
let revealCheckbox = this.shadowRoot.querySelector(".reveal-checkbox");
revealCheckbox.checked = false;
}
checkValidity() {
if (!this.hasAttribute("required")) {
return true;
}
let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
return unlockedValue.checkValidity();
}
reportValidity() {
if (!this.hasAttribute("required")) {
return true;
}
let unlockedValue = this.shadowRoot.querySelector(".unlocked-value");
return unlockedValue.reportValidity();
}
updateRevealCheckboxTitle() {
let revealCheckbox = this.shadowRoot.querySelector(".reveal-checkbox");
let labelAttr = revealCheckbox.checked ? "reveal-checkbox-hide"
: "reveal-checkbox-show";
revealCheckbox.setAttribute("aria-label", this.getAttribute(labelAttr));
revealCheckbox.setAttribute("title", this.getAttribute(labelAttr));
}
}
customElements.define("modal-input", ModalInput);

View File

@ -15,8 +15,6 @@ browser.jar:
content/browser/aboutlogins/components/login-list-item.js (content/components/login-list-item.js)
content/browser/aboutlogins/components/menu-button.css (content/components/menu-button.css)
content/browser/aboutlogins/components/menu-button.js (content/components/menu-button.js)
content/browser/aboutlogins/components/modal-input.css (content/components/modal-input.css)
content/browser/aboutlogins/components/modal-input.js (content/components/modal-input.js)
content/browser/aboutlogins/components/reflected-fluent-element.js (content/components/reflected-fluent-element.js)
content/browser/aboutlogins/icons/delete.svg (content/icons/delete.svg)
content/browser/aboutlogins/icons/edit.svg (content/icons/edit.svg)

View File

@ -35,6 +35,7 @@ add_task(async function test() {
expectedValue: testCase[0],
copyButtonSelector: testCase[1],
};
info("waiting for " + testObj.expectedValue + " to be placed on clipboard");
await SimpleTest.promiseClipboardChange(testObj.expectedValue, async () => {
await ContentTask.spawn(browser, testObj, async function(aTestObj) {
let loginItem = content.document.querySelector("login-item");
@ -44,7 +45,7 @@ add_task(async function test() {
innerButton.click();
});
});
ok(true, "Username is on clipboard now");
ok(true, testObj.expectedValue + " is on clipboard now");
await ContentTask.spawn(browser, testObj, async function(aTestObj) {
let loginItem = content.document.querySelector("login-item");

View File

@ -33,10 +33,8 @@ add_task(async function test_create_login() {
let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
let originInput = loginItem.shadowRoot.querySelector("input[name='origin']");
let usernameInput = loginItem.shadowRoot.querySelector("modal-input[name='username']")
.shadowRoot.querySelector(".unlocked-value");
let passwordInput = loginItem.shadowRoot.querySelector("modal-input[name='password']")
.shadowRoot.querySelector(".unlocked-value");
let usernameInput = loginItem.shadowRoot.querySelector("input[name='username']");
let passwordInput = loginItem.shadowRoot.querySelector("input[name='password']");
originInput.value = aOriginTuple[0];
usernameInput.value = "testuser1";
@ -74,10 +72,8 @@ add_task(async function test_create_login() {
let editButton = loginItem.shadowRoot.querySelector(".edit-button");
editButton.click();
let usernameInput = loginItem.shadowRoot.querySelector("modal-input[name='username']")
.shadowRoot.querySelector(".unlocked-value");
let passwordInput = loginItem.shadowRoot.querySelector("modal-input[name='password']")
.shadowRoot.querySelector(".unlocked-value");
let usernameInput = loginItem.shadowRoot.querySelector("input[name='username']");
let passwordInput = loginItem.shadowRoot.querySelector("input[name='password']");
usernameInput.value = "testuser2";
passwordInput.value = "testpass2";

View File

@ -44,8 +44,8 @@ add_task(async function test_login_item() {
}, "Waiting for login item to get populated");
ok(loginItemPopulated, "The login item should get populated");
let usernameInput = loginItem.shadowRoot.querySelector("modal-input[name='username']");
let passwordInput = loginItem.shadowRoot.querySelector("modal-input[name='password']");
let usernameInput = loginItem.shadowRoot.querySelector("input[name='username']");
let passwordInput = loginItem.shadowRoot.querySelector("input[name='password']");
let editButton = loginItem.shadowRoot.querySelector(".edit-button");
editButton.click();
@ -56,7 +56,8 @@ add_task(async function test_login_item() {
let cancelButton = loginItem.shadowRoot.querySelector(".cancel-button");
cancelButton.click();
await Promise.resolve();
usernameInput = loginItem.shadowRoot.querySelector("input[name='username']");
passwordInput = loginItem.shadowRoot.querySelector("input[name='password']");
is(usernameInput.value, login.username, "Username change should be reverted");
is(passwordInput.value, login.password, "Password change should be reverted");
@ -71,6 +72,8 @@ add_task(async function test_login_item() {
let saveChangesButton = loginItem.shadowRoot.querySelector(".save-changes-button");
saveChangesButton.click();
usernameInput = loginItem.shadowRoot.querySelector("input[name='username']");
passwordInput = loginItem.shadowRoot.querySelector("input[name='password']");
await ContentTaskUtils.waitForCondition(() => {
loginListItem = Cu.waiveXrays(loginList.shadowRoot.querySelector("login-list-item"));
return loginListItem._login.username == usernameInput.value &&

View File

@ -16,5 +16,4 @@ support-files =
[test_login_item.html]
[test_login_list.html]
[test_menu_button.html]
[test_modal_input.html]
[test_reflected_fluent_element.html]

View File

@ -9,7 +9,6 @@ Test the login-item component
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script type="module" src="login-item.js"></script>
<script type="module" src="modal-input.js"></script>
<script src="aboutlogins_common.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
@ -64,9 +63,9 @@ add_task(async function setup() {
add_task(async function test_empty_item() {
ok(gLoginItem, "loginItem exists");
is(gLoginItem.shadowRoot.querySelector(".origin-saved-value").textContent, "", "origin should be blank");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, "", "username should be blank");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, "", "password should be blank");
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin 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 when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank when undefined");
@ -78,13 +77,9 @@ add_task(async function test_set_login() {
ok(!gLoginItem.hasAttribute("editing"), "loginItem should not be in 'edit' mode");
ok(!gLoginItem.hasAttribute("isNewLogin"), "loginItem should not be in 'isNewLogin' mode");
let savedOrigin = gLoginItem.shadowRoot.querySelector(".origin-saved-value");
is(getComputedStyle(savedOrigin).display, "inline", ".origin-saved-value should be visible for non-editing existing logins");
let originInput = gLoginItem.shadowRoot.querySelector("input[name='origin']");
is(getComputedStyle(originInput).display, "none", "input[name='origin'] should be hidden for non-editing existing logins");
is(savedOrigin.textContent, TEST_LOGIN_1.origin, "origin should be populated");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin 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");
@ -98,21 +93,17 @@ add_task(async function test_edit_login() {
ok(gLoginItem.hasAttribute("editing"), "loginItem should be in 'edit' mode");
ok(isHidden(gLoginItem.shadowRoot.querySelector(".edit-button")), "edit button should be hidden in 'edit' mode");
ok(!gLoginItem.hasAttribute("isNewLogin"), "loginItem should not be in 'isNewLogin' mode");
let savedOrigin = gLoginItem.shadowRoot.querySelector(".origin-saved-value");
is(getComputedStyle(savedOrigin).display, "inline", ".origin-saved-value should be visible for editing existing logins");
let deleteButton = gLoginItem.shadowRoot.querySelector(".delete-button");
ok(!deleteButton.disabled, "Delete button should be enabled when editing a login");
let originInput = gLoginItem.shadowRoot.querySelector("input[name='origin']");
is(getComputedStyle(originInput).display, "none", "input[name='origin'] should be hidden for editing existing logins");
is(savedOrigin.textContent, TEST_LOGIN_1.origin, "origin should be populated");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin 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");
gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value = "newUsername";
gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value = "newPassword";
gLoginItem.shadowRoot.querySelector("input[name='username']").value = "newUsername";
gLoginItem.shadowRoot.querySelector("input[name='password']").value = "newPassword";
let updateEventDispatched = false;
document.addEventListener("AboutLoginsUpdateLogin", event => {
@ -145,8 +136,7 @@ add_task(async function test_edit_login_cancel() {
add_task(async function test_reveal_password_change_selected_login() {
gLoginItem.setLogin(TEST_LOGIN_1);
let passwordInput = gLoginItem.shadowRoot.querySelector('modal-input[name="password"]');
let revealCheckbox = passwordInput.shadowRoot.querySelector(".reveal-checkbox");
let revealCheckbox = gLoginItem.shadowRoot.querySelector(".reveal-password-checkbox");
ok(!revealCheckbox.checked, "reveal-checkbox should not be checked by default");
revealCheckbox.click();
@ -163,14 +153,11 @@ add_task(async function test_set_login_empty() {
ok(gLoginItem.hasAttribute("editing"), "loginItem should be in 'edit' mode");
ok(isHidden(gLoginItem.shadowRoot.querySelector(".edit-button")), "edit button should be hidden in 'edit' mode");
ok(gLoginItem.hasAttribute("isNewLogin"), "loginItem should be in 'isNewLogin' mode");
let savedOrigin = gLoginItem.shadowRoot.querySelector(".origin-saved-value");
is(getComputedStyle(savedOrigin).display, "none", ".origin-saved-value should be hidden for new logins");
let originInput = gLoginItem.shadowRoot.querySelector("input[name='origin']");
let deleteButton = gLoginItem.shadowRoot.querySelector(".delete-button");
ok(deleteButton.disabled, "Delete button should be disabled when creating a login");
is(getComputedStyle(originInput).display, "inline-block", "input[name='origin'] should be visible for new logins");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, "", "username should be empty");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, "", "password should be empty");
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be empty");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be empty");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be empty");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank when undefined");
@ -181,6 +168,7 @@ add_task(async function test_set_login_empty() {
}, {once: true});
gLoginItem.shadowRoot.querySelector(".save-changes-button").click();
ok(!createEventDispatched, "Clicking the .save-changes-button shouldn't dispatch the event when fields are invalid");
let originInput = gLoginItem.shadowRoot.querySelector("input[name='origin']");
ok(originInput.matches(":invalid"), "origin value is required");
is(originInput.value, "", "origin input should be blank at start");
@ -200,8 +188,8 @@ add_task(async function test_set_login_empty() {
synthesizeKey("VK_TAB", { shiftKey: true });
}
gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value = "user1";
gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value = "pass1";
gLoginItem.shadowRoot.querySelector("input[name='username']").value = "user1";
gLoginItem.shadowRoot.querySelector("input[name='password']").value = "pass1";
document.addEventListener("AboutLoginsCreateLogin", event => {
is(event.detail.guid, undefined, "event should not include guid");
@ -220,9 +208,9 @@ add_task(async function test_different_login_modified() {
gLoginItem.loginModified(otherLogin);
await asyncElementRendered();
is(gLoginItem.shadowRoot.querySelector(".origin-saved-value").textContent, TEST_LOGIN_1.origin, "origin should be unchanged");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin 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");
@ -234,9 +222,9 @@ add_task(async function test_different_login_removed() {
gLoginItem.loginRemoved(otherLogin);
await asyncElementRendered();
is(gLoginItem.shadowRoot.querySelector(".origin-saved-value").textContent, TEST_LOGIN_1.origin, "origin should be unchanged");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin 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");
@ -248,9 +236,9 @@ add_task(async function test_login_modified() {
gLoginItem.loginModified(modifiedLogin);
await asyncElementRendered();
is(gLoginItem.shadowRoot.querySelector(".origin-saved-value").textContent, modifiedLogin.origin, "origin should be updated");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, modifiedLogin.username, "username should be updated");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, modifiedLogin.password, "password should be updated");
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, modifiedLogin.origin, "origin 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");
@ -261,9 +249,9 @@ add_task(async function test_login_removed() {
gLoginItem.loginRemoved(TEST_LOGIN_1);
await asyncElementRendered();
is(gLoginItem.shadowRoot.querySelector(".origin-saved-value").textContent, "", "origin should be cleared");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='username']").value, "", "username should be cleared");
is(gLoginItem.shadowRoot.querySelector("modal-input[name='password']").value, "", "password should be cleared");
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin 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 blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank when undefined");

View File

@ -1,100 +0,0 @@
<!DOCTYPE HTML>
<html>
<!--
Test the modal-input component
-->
<head>
<meta charset="utf-8">
<title>Test the modal-input component</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="module" src="modal-input.js"></script>
<script src="aboutlogins_common.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<p id="display">
</p>
<div id="content" style="display: none">
<iframe id="templateFrame" src="aboutLogins.html"
sandbox="allow-same-origin"></iframe>
</div>
<pre id="test">
</pre>
<script>
/** Test the modal-input component **/
let gModalInput;
const TEST_INPUT_VALUE = "fakeValue";
add_task(async function setup() {
let templateFrame = document.getElementById("templateFrame");
let displayEl = document.getElementById("display");
importDependencies(templateFrame, displayEl);
gModalInput = document.createElement("modal-input");
gModalInput.setAttribute("value", TEST_INPUT_VALUE);
displayEl.appendChild(gModalInput);
});
add_task(async function test_initial_state() {
ok(gModalInput, "modalInput exists");
is(gModalInput.shadowRoot.querySelector(".locked-value").textContent, TEST_INPUT_VALUE, "Values are set initially");
is(gModalInput.shadowRoot.querySelector(".unlocked-value").defaultValue, TEST_INPUT_VALUE, "Values are set initially");
is(getComputedStyle(gModalInput.shadowRoot.querySelector(".locked-value")).display, "block", ".locked-value is visible by default");
is(getComputedStyle(gModalInput.shadowRoot.querySelector(".unlocked-value")).display, "none", ".unlocked-value is hidden by default");
});
add_task(async function test_editing_set_unset() {
let lockedValue = gModalInput.shadowRoot.querySelector(".locked-value");
let unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
gModalInput.setAttribute("editing", "");
is(getComputedStyle(lockedValue).display, "none", ".locked-value is hidden when editing");
is(getComputedStyle(unlockedValue).display, "block", ".unlocked-value is visible when editing");
const NEW_VALUE = "editedValue";
SpecialPowers.wrap(unlockedValue).setUserInput(NEW_VALUE);
gModalInput.removeAttribute("editing");
unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
is(lockedValue.textContent, TEST_INPUT_VALUE, "Values are restored to value prior to edit");
is(unlockedValue.value, TEST_INPUT_VALUE, "Edited value is cleared after 'edit' mode is left");
is(unlockedValue.defaultValue, TEST_INPUT_VALUE, "Default value is maintained");
is(gModalInput.getAttribute("value"), TEST_INPUT_VALUE, "The value attribute on the host element is restored");
is(getComputedStyle(lockedValue).display, "block", ".locked-value is visible when not editing");
unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
is(getComputedStyle(unlockedValue).display, "none", ".unlocked-value is hidden when not editing");
});
add_task(async function test_password() {
gModalInput.setAttribute("type", "password");
let lockedValue = gModalInput.shadowRoot.querySelector(".locked-value");
let unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
is(lockedValue.textContent, gModalInput.constructor.LOCKED_PASSWORD_DISPLAY,
"type=password should display masked characters when locked");
is(unlockedValue.defaultValue, gModalInput.getAttribute("value"), "type=password should have actual value in .unlocked-value");
is(unlockedValue.getAttribute("type"), "password", "input[type=password] should be used for .unlocked-value with type=password");
gModalInput.removeAttribute("value");
is(lockedValue.textContent, "",
"type=password should display nothing when locked without a value (.locked-value)");
unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
is(unlockedValue.value, "",
"type=password should display nothing when locked without a value (.unlocked-value)");
});
add_task(async function test_required() {
let unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
ok(unlockedValue.checkValidity(), "The modal-input should be valid when before it is required");
gModalInput.setAttribute("required", "");
ok(!unlockedValue.checkValidity(), "Setting 'required' on the modal-input should make the unlocked-value required");
unlockedValue = gModalInput.shadowRoot.querySelector(".unlocked-value");
unlockedValue.value = "foo";
ok(unlockedValue.checkValidity(), "Setting a value on the required modal-input should pass validation");
});
</script>
</body>
</html>