Bug 1901308 - Show profile backup errors and keep modals open r=backup-reviewers,fluent-reviewers,firefox-desktop-core-reviewers ,mconley,bolsson

Currently, clicking "confirm" buttons on modals in the profile backup settings menu will always close the modals regardless of whether the operation succeeded or failed. In the case of errors, users don't know that something went wrong. It's better to keep the modals open and display an error so that the user knows what to do next and can try to fix the issue, if applicable.

Differential Revision: https://phabricator.services.mozilla.com/D218358
This commit is contained in:
Stephen Thompson 2024-08-06 18:21:16 +00:00
parent ab8fa1ab81
commit 6c1381ca4b
23 changed files with 397 additions and 298 deletions

View File

@ -456,12 +456,14 @@ let JSWINDOWACTORS = {
esModuleURI: "resource:///actors/BackupUIChild.sys.mjs",
events: {
"BackupUI:InitWidget": { wantUntrusted: true },
"BackupUI:ToggleScheduledBackups": { wantUntrusted: true },
"BackupUI:EnableScheduledBackups": { wantUntrusted: true },
"BackupUI:DisableScheduledBackups": { wantUntrusted: true },
"BackupUI:ShowFilepicker": { wantUntrusted: true },
"BackupUI:GetBackupFileInfo": { wantUntrusted: true },
"BackupUI:RestoreFromBackupFile": { wantUntrusted: true },
"BackupUI:RestoreFromBackupChooseFile": { wantUntrusted: true },
"BackupUI:ToggleEncryption": { wantUntrusted: true },
"BackupUI:EnableEncryption": { wantUntrusted: true },
"BackupUI:DisableEncryption": { wantUntrusted: true },
"BackupUI:RerunEncryption": { wantUntrusted: true },
"BackupUI:ShowBackupLocation": { wantUntrusted: true },
"BackupUI:EditBackupLocation": { wantUntrusted: true },

View File

@ -2753,6 +2753,7 @@ export class BackupService extends EventTarget {
lazy.logConsole.error(
`Failed to set parent directory ${parentDirPath}. ${e}`
);
throw e;
}
}

View File

@ -29,8 +29,26 @@ export class BackupUIChild extends JSWindowActorChild {
if (event.type == "BackupUI:InitWidget") {
this.#inittedWidgets.add(event.target);
this.sendAsyncMessage("RequestState");
} else if (event.type == "BackupUI:ToggleScheduledBackups") {
this.sendAsyncMessage("ToggleScheduledBackups", event.detail);
} else if (event.type == "BackupUI:EnableScheduledBackups") {
const target = event.target;
const result = await this.sendQuery(
"EnableScheduledBackups",
event.detail
);
if (result.success) {
target.close();
} else {
target.enableBackupErrorCode = result.errorCode;
}
} else if (event.type == "BackupUI:DisableScheduledBackups") {
const target = event.target;
this.sendAsyncMessage("DisableScheduledBackups", event.detail);
// backups will always end up disabled even if there was an error
// with other bookkeeping related to turning off backups
target.close();
} else if (event.type == "BackupUI:ShowFilepicker") {
let targetNodeName = event.target.nodeName;
let { path, filename, iconURL } = await this.sendQuery("ShowFilepicker", {
@ -79,10 +97,33 @@ export class BackupUIChild extends JSWindowActorChild {
}
} else if (event.type == "BackupUI:RestoreFromBackupChooseFile") {
this.sendAsyncMessage("RestoreFromBackupChooseFile");
} else if (event.type == "BackupUI:ToggleEncryption") {
this.sendAsyncMessage("ToggleEncryption", event.detail);
} else if (event.type == "BackupUI:EnableEncryption") {
const target = event.target;
const result = await this.sendQuery("EnableEncryption", event.detail);
if (result.success) {
target.close();
} else {
target.enableEncryptionErrorCode = result.errorCode;
}
} else if (event.type == "BackupUI:DisableEncryption") {
const target = event.target;
const result = await this.sendQuery("DisableEncryption", event.detail);
if (result.success) {
target.close();
} else {
target.disableEncryptionErrorCode = result.errorCode;
}
} else if (event.type == "BackupUI:RerunEncryption") {
this.sendAsyncMessage("RerunEncryption", event.detail);
const target = event.target;
const result = await this.sendQuery("RerunEncryption", event.detail);
if (result.success) {
target.close();
} else {
target.rerunEncryptionErrorCode = result.errorCode;
}
} else if (event.type == "BackupUI:ShowBackupLocation") {
this.sendAsyncMessage("ShowBackupLocation");
} else if (event.type == "BackupUI:EditBackupLocation") {

View File

@ -82,50 +82,37 @@ export class BackupUIParent extends JSWindowActorParent {
async receiveMessage(message) {
if (message.name == "RequestState") {
this.sendState();
} else if (message.name == "ToggleScheduledBackups") {
let { isScheduledBackupsEnabled, parentDirPath, password } = message.data;
if (isScheduledBackupsEnabled) {
} else if (message.name == "EnableScheduledBackups") {
try {
let { parentDirPath, password } = message.data;
if (parentDirPath) {
this.#bs.setParentDirPath(parentDirPath);
/**
* TODO: display an error and do not attempt to toggle scheduled backups if there
* is a problem with setting the parent directory (bug 1901308).
*/
}
if (password) {
try {
await this.#bs.enableEncryption(password);
} catch (e) {
/**
* TODO: display en error and do not attempt to toggle scheduled backups if there is a
* problem with enabling encryption (bug 1901308)
*/
return null;
}
}
} else {
try {
if (this.#bs.state.encryptionEnabled) {
await this.#bs.disableEncryption();
}
await this.#bs.deleteLastBackup();
} catch (e) {
// no-op so that scheduled backups can still be turned off
await this.#bs.enableEncryption(password);
}
this.#bs.setScheduledBackups(true);
} catch (e) {
lazy.logConsole.error(`Failed to enable scheduled backups`, e);
return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN };
}
this.#bs.setScheduledBackups(isScheduledBackupsEnabled);
return true;
/**
* TODO: (Bug 1900125) we should create a backup at the specified dir path once we turn on
* scheduled backups. The backup folder in the chosen directory should contain
* the archive file, which we create using BackupService.createArchive implemented in
* Bug 1897498.
*/
return { success: true };
} else if (message.name == "DisableScheduledBackups") {
try {
if (this.#bs.state.encryptionEnabled) {
await this.#bs.disableEncryption();
}
await this.#bs.deleteLastBackup();
} catch (e) {
// no-op so that scheduled backups can still be turned off
}
this.#bs.setScheduledBackups(false);
} else if (message.name == "ShowFilepicker") {
let { win, filter, displayDirectoryPath } = message.data;
@ -191,52 +178,45 @@ export class BackupUIParent extends JSWindowActorParent {
return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN };
}
return { success: true };
} else if (message.name == "ToggleEncryption") {
let { isEncryptionEnabled, password } = message.data;
if (!isEncryptionEnabled) {
try {
await this.#bs.disableEncryption();
/**
* TODO: (Bug 1901640) after disabling encryption, recreate the backup,
* this time without sensitive data.
*/
} catch (e) {
/**
* TODO: (Bug 1901308) maybe display an error if there is a problem with
* disabling encryption.
*/
}
} else {
try {
await this.#bs.enableEncryption(password);
/**
* TODO: (Bug 1901640) after enabling encryption, recreate the backup,
* this time with sensitive data.
*/
} catch (e) {
/**
* TODO: (Bug 1901308) maybe display an error if there is a problem with
* enabling encryption.
*/
}
} else if (message.name == "EnableEncryption") {
try {
await this.#bs.enableEncryption(message.data.password);
} catch (e) {
lazy.logConsole.error(`Failed to enable encryption`, e);
return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN };
}
} else if (message.name == "RerunEncryption") {
let { password } = message.data;
/**
* TODO: (Bug 1901640) after enabling encryption, recreate the backup,
* this time with sensitive data.
*/
return { success: true };
} else if (message.name == "DisableEncryption") {
try {
await this.#bs.disableEncryption();
await this.#bs.enableEncryption(password);
/**
* TODO: (Bug 1901640) after enabling encryption, recreate the backup,
* this time with the new password.
*/
} catch (e) {
/**
* TODO: (Bug 1901308) maybe display an error if there is a problem with
* re-encryption.
*/
lazy.logConsole.error(`Failed to disable encryption`, e);
return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN };
}
/**
* TODO: (Bug 1901640) after disabling encryption, recreate the backup,
* this time without sensitive data.
*/
return { success: true };
} else if (message.name == "RerunEncryption") {
try {
let { password } = message.data;
await this.#bs.disableEncryption();
await this.#bs.enableEncryption(password);
} catch (e) {
lazy.logConsole.error(`Failed to rerun encryption`, e);
return { success: false, errorCode: e.cause || lazy.ERRORS.UNKNOWN };
}
/**
* TODO: (Bug 1901640) after enabling encryption, recreate the backup,
* this time with the new password.
*/
return { success: true };
} else if (message.name == "ShowBackupLocation") {
this.#bs.showBackupLocation();
} else if (message.name == "EditBackupLocation") {

View File

@ -93,44 +93,14 @@ export default class BackupSettings extends MozLitElement {
new CustomEvent("BackupUI:InitWidget", { bubbles: true })
);
this.addEventListener("turnOnScheduledBackups", this);
this.addEventListener("turnOffScheduledBackups", this);
this.addEventListener("dialogCancel", this);
this.addEventListener("getBackupFileInfo", this);
this.addEventListener("enableEncryption", this);
this.addEventListener("rerunEncryption", this);
this.addEventListener("disableEncryption", this);
this.addEventListener("restoreFromBackupConfirm", this);
this.addEventListener("restoreFromBackupChooseFile", this);
}
handleEvent(event) {
switch (event.type) {
case "turnOnScheduledBackups":
this.turnOnScheduledBackupsDialogEl.close();
this.dispatchEvent(
new CustomEvent("BackupUI:ToggleScheduledBackups", {
bubbles: true,
composed: true,
detail: {
...event.detail,
isScheduledBackupsEnabled: true,
},
})
);
break;
case "turnOffScheduledBackups":
this.turnOffScheduledBackupsDialogEl.close();
this.dispatchEvent(
new CustomEvent("BackupUI:ToggleScheduledBackups", {
bubbles: true,
composed: true,
detail: {
isScheduledBackupsEnabled: false,
},
})
);
break;
case "dialogCancel":
if (this.turnOnScheduledBackupsDialogEl.open) {
this.turnOnScheduledBackupsDialogEl.close();
@ -175,43 +145,6 @@ export default class BackupSettings extends MozLitElement {
})
);
break;
case "enableEncryption":
this.enableBackupEncryptionDialogEl.close();
this.dispatchEvent(
new CustomEvent("BackupUI:ToggleEncryption", {
bubbles: true,
composed: true,
detail: {
...event.detail,
isEncryptionEnabled: true,
},
})
);
break;
case "rerunEncryption":
this.enableBackupEncryptionDialogEl.close();
this.dispatchEvent(
new CustomEvent("BackupUI:RerunEncryption", {
bubbles: true,
composed: true,
detail: {
...event.detail,
},
})
);
break;
case "disableEncryption":
this.disableBackupEncryptionDialogEl.close();
this.dispatchEvent(
new CustomEvent("BackupUI:ToggleEncryption", {
bubbles: true,
composed: true,
detail: {
isEncryptionEnabled: false,
},
})
);
break;
}
}

View File

@ -5,47 +5,66 @@
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-message-bar.mjs";
const ERROR_L10N_ID = "backup-error-retry";
/**
* The widget for disabling password protection if the backup is already
* encrypted.
*/
export default class DisableBackupEncryption extends MozLitElement {
static properties = {
// managed by BackupUIChild
disableEncryptionErrorCode: { type: Number },
};
static get queries() {
return {
cancelButtonEl: "#backup-disable-encryption-cancel-button",
confirmButtonEl: "#backup-disable-encryption-confirm-button",
errorEl: "#disable-backup-encryption-error",
};
}
/**
* Dispatches the BackupUI:InitWidget custom event upon being attached to the
* DOM, which registers with BackupUIChild for BackupService state updates.
*/
connectedCallback() {
super.connectedCallback();
this.dispatchEvent(
new CustomEvent("BackupUI:InitWidget", { bubbles: true })
);
constructor() {
super();
this.disableEncryptionErrorCode = 0;
}
handleCancel() {
close() {
this.dispatchEvent(
new CustomEvent("dialogCancel", {
bubbles: true,
composed: true,
})
);
this.reset();
}
reset() {
this.disableEncryptionErrorCode = 0;
}
handleConfirm() {
this.dispatchEvent(
new CustomEvent("disableEncryption", {
new CustomEvent("BackupUI:DisableEncryption", {
bubbles: true,
composed: true,
})
);
}
errorTemplate() {
return html`
<moz-message-bar
id="disable-backup-encryption-error"
type="error"
.messageL10nId="${ERROR_L10N_ID}"
></moz-message-bar>
`;
}
contentTemplate() {
return html`
<div
@ -73,12 +92,13 @@ export default class DisableBackupEncryption extends MozLitElement {
data-l10n-id="disable-backup-encryption-support-link"
></a>
</div>
${this.disableEncryptionErrorCode ? this.errorTemplate() : null}
</main>
<moz-button-group id="backup-disable-encryption-button-group">
<moz-button
id="backup-disable-encryption-cancel-button"
@click=${this.handleCancel}
@click=${this.close}
data-l10n-id="disable-backup-encryption-cancel-button"
></moz-button>
<moz-button

View File

@ -5,21 +5,40 @@
// eslint-disable-next-line import/no-unresolved
import { html } from "lit.all.mjs";
import "chrome://global/content/elements/moz-card.mjs";
import { ERRORS } from "chrome://browser/content/backup/backup-constants.mjs";
import "./disable-backup-encryption.mjs";
window.MozXULElement.insertFTLIfNeeded("locales-preview/backupSettings.ftl");
window.MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
const SELECTABLE_ERRORS = {
"(none)": 0,
...ERRORS,
};
export default {
title: "Domain-specific UI Widgets/Backup/Disable Encryption",
component: "disable-backup-encryption",
argTypes: {},
argTypes: {
disableEncryptionErrorCode: {
options: Object.keys(SELECTABLE_ERRORS),
mapping: SELECTABLE_ERRORS,
control: { type: "select" },
},
},
};
const Template = () => html`
const Template = ({ disableEncryptionErrorCode }) => html`
<moz-card style="width: 23.94rem;">
<disable-backup-encryption></disable-backup-encryption>
<disable-backup-encryption
.disableEncryptionErrorCode=${disableEncryptionErrorCode}
></disable-backup-encryption>
</moz-card>
`;
export const Default = Template.bind({});
export const DisableError = Template.bind({});
DisableError.args = {
disableEncryptionErrorCode: ERRORS.UNKNOWN,
};

View File

@ -5,9 +5,13 @@
import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-message-bar.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/backup/password-validation-inputs.mjs";
import { ERRORS } from "chrome://browser/content/backup/backup-constants.mjs";
/**
* Valid attributes for the enable-backup-encryption dialog type.
*
@ -23,14 +27,30 @@ const VALID_L10N_IDS = new Map([
[VALID_TYPES.CHANGE_PASSWORD, "change-backup-encryption-header"],
]);
const ERROR_L10N_IDS = Object.freeze({
[ERRORS.INVALID_PASSWORD]: "backup-error-password-requirements",
[ERRORS.UNKNOWN]: "backup-error-retry",
});
/**
* @param {number} errorCode Error code from backup-constants.mjs
* @returns {string} Localization ID for error message
*/
function getErrorL10nId(errorCode) {
return ERROR_L10N_IDS[errorCode] ?? ERROR_L10N_IDS[ERRORS.UNKNOWN];
}
/**
* The widget for enabling password protection if the backup is not yet
* encrypted.
*/
export default class EnableBackupEncryption extends MozLitElement {
static properties = {
// internal state
_inputPassValue: { type: String, state: true },
_passwordsMatch: { type: Boolean, state: true },
// passed from parents
supportBaseLink: { type: String },
/**
* The "type" attribute changes the layout.
@ -38,6 +58,10 @@ export default class EnableBackupEncryption extends MozLitElement {
* @see VALID_TYPES
*/
type: { type: String, reflect: true },
// managed by BackupUIChild
enableEncryptionErrorCode: { type: Number },
rerunEncryptionErrorCode: { type: Number },
};
static get queries() {
@ -48,6 +72,7 @@ export default class EnableBackupEncryption extends MozLitElement {
textHeaderEl: "#backup-enable-encryption-header",
textDescriptionEl: "#backup-enable-encryption-description",
passwordInputsEl: "#backup-enable-encryption-password-inputs",
errorEl: "#enable-backup-encryption-error",
};
}
@ -57,18 +82,13 @@ export default class EnableBackupEncryption extends MozLitElement {
this.type = VALID_TYPES.SET_PASSWORD;
this._inputPassValue = "";
this._passwordsMatch = false;
this.enableEncryptionErrorCode = 0;
this.rerunEncryptionErrorCode = 0;
}
/**
* Dispatches the BackupUI:InitWidget custom event upon being attached to the
* DOM, which registers with BackupUIChild for BackupService state updates.
*/
connectedCallback() {
super.connectedCallback();
this.dispatchEvent(
new CustomEvent("BackupUI:InitWidget", { bubbles: true })
);
// Listening to events from child <password-validation-inputs>
this.addEventListener("ValidPasswordsDetected", this);
this.addEventListener("InvalidPasswordsDetected", this);
}
@ -84,23 +104,29 @@ export default class EnableBackupEncryption extends MozLitElement {
}
}
handleCancel() {
close() {
this.dispatchEvent(
new CustomEvent("dialogCancel", {
bubbles: true,
composed: true,
})
);
this.resetChanges();
this.reset();
}
reset() {
this._inputPassValue = "";
this._passwordsMatch = false;
this.passwordInputsEl.reset();
this.enableEncryptionErrorCode = 0;
}
handleConfirm() {
switch (this.type) {
case VALID_TYPES.SET_PASSWORD:
this.dispatchEvent(
new CustomEvent("enableEncryption", {
new CustomEvent("BackupUI:EnableEncryption", {
bubbles: true,
composed: true,
detail: {
password: this._inputPassValue,
},
@ -109,9 +135,8 @@ export default class EnableBackupEncryption extends MozLitElement {
break;
case VALID_TYPES.CHANGE_PASSWORD:
this.dispatchEvent(
new CustomEvent("rerunEncryption", {
new CustomEvent("BackupUI:RerunEncryption", {
bubbles: true,
composed: true,
detail: {
password: this._inputPassValue,
},
@ -119,16 +144,6 @@ export default class EnableBackupEncryption extends MozLitElement {
);
break;
}
this.resetChanges();
}
resetChanges() {
this._inputPassValue = "";
this._passwordsMatch = false;
this.passwordInputsEl.dispatchEvent(
new CustomEvent("resetInputs", { bubbles: true, composed: true })
);
}
descriptionTemplate() {
@ -155,7 +170,7 @@ export default class EnableBackupEncryption extends MozLitElement {
<moz-button-group id="backup-enable-encryption-button-group">
<moz-button
id="backup-enable-encryption-cancel-button"
@click=${this.handleCancel}
@click=${this.close}
data-l10n-id="enable-backup-encryption-cancel-button"
></moz-button>
<moz-button
@ -169,6 +184,19 @@ export default class EnableBackupEncryption extends MozLitElement {
`;
}
errorTemplate() {
let messageId = this.enableEncryptionErrorCode
? getErrorL10nId(this.enableEncryptionErrorCode)
: getErrorL10nId(this.rerunEncryptionErrorCode);
return html`
<moz-message-bar
id="enable-backup-encryption-error"
type="error"
.messageL10nId="${messageId}"
></moz-message-bar>
`;
}
contentTemplate() {
return html`
<div
@ -190,6 +218,10 @@ export default class EnableBackupEncryption extends MozLitElement {
.supportBaseLink=${this.supportBaseLink}
>
</password-validation-inputs>
${this.enableEncryptionErrorCode || this.rerunEncryptionErrorCode
? this.errorTemplate()
: null}
</div>
${this.buttonGroupTemplate()}
</div>

View File

@ -5,11 +5,17 @@
// eslint-disable-next-line import/no-unresolved
import { html } from "lit.all.mjs";
import "chrome://global/content/elements/moz-card.mjs";
import { ERRORS } from "chrome://browser/content/backup/backup-constants.mjs";
import "./enable-backup-encryption.mjs";
window.MozXULElement.insertFTLIfNeeded("locales-preview/backupSettings.ftl");
window.MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
const SELECTABLE_ERRORS = {
"(none)": 0,
...ERRORS,
};
export default {
title: "Domain-specific UI Widgets/Backup/Enable Encryption",
component: "enable-backup-encryption",
@ -18,12 +24,30 @@ export default {
control: { type: "select" },
options: ["set-password", "change-password"],
},
enableEncryptionErrorCode: {
options: Object.keys(SELECTABLE_ERRORS),
mapping: SELECTABLE_ERRORS,
control: { type: "select" },
},
rerunEncryptionErrorCode: {
options: Object.keys(SELECTABLE_ERRORS),
mapping: SELECTABLE_ERRORS,
control: { type: "select" },
},
},
};
const Template = ({ type }) => html`
const Template = ({
type,
enableEncryptionErrorCode,
rerunEncryptionErrorCode,
}) => html`
<moz-card style="width: 23.94rem; position: relative;">
<enable-backup-encryption type=${type}></enable-backup-encryption>
<enable-backup-encryption
type=${type}
.enableEncryptionErrorCode=${enableEncryptionErrorCode}
.rerunEncryptionErrorCode=${rerunEncryptionErrorCode}
></enable-backup-encryption>
</moz-card>
`;
@ -36,3 +60,15 @@ export const ChangePassword = Template.bind({});
ChangePassword.args = {
type: "change-password",
};
export const SetPasswordError = Template.bind({});
SetPasswordError.args = {
type: "set-password",
enableEncryptionErrorCode: ERRORS.INVALID_PASSWORD,
};
export const ChangePasswordError = Template.bind({});
ChangePasswordError.args = {
type: "change-password",
rerunEncryptionErrorCode: ERRORS.INVALID_PASSWORD,
};

View File

@ -48,17 +48,7 @@ export default class PasswordValidationInputs extends MozLitElement {
this._tooltipFocus = false;
}
connectedCallback() {
super.connectedCallback();
this.addEventListener("resetInputs", this.handleReset);
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("resetInputs", this.handleReset);
}
handleReset() {
reset() {
this.formEl.reset();
this._showRules = false;
this._hasCommon = false;

View File

@ -17,18 +17,7 @@ export default class TurnOffScheduledBackups extends MozLitElement {
};
}
/**
* Dispatches the BackupUI:InitWidget custom event upon being attached to the
* DOM, which registers with BackupUIChild for BackupService state updates.
*/
connectedCallback() {
super.connectedCallback();
this.dispatchEvent(
new CustomEvent("BackupUI:InitWidget", { bubbles: true })
);
}
handleCancel() {
close() {
this.dispatchEvent(
new CustomEvent("dialogCancel", {
bubbles: true,
@ -39,9 +28,8 @@ export default class TurnOffScheduledBackups extends MozLitElement {
handleConfirm() {
this.dispatchEvent(
new CustomEvent("turnOffScheduledBackups", {
new CustomEvent("BackupUI:DisableScheduledBackups", {
bubbles: true,
composed: true,
})
);
}
@ -77,7 +65,7 @@ export default class TurnOffScheduledBackups extends MozLitElement {
<moz-button-group id="backup-turn-off-scheduled-button-group">
<moz-button
id="backup-turn-off-scheduled-cancel-button"
@click=${this.handleCancel}
@click=${this.close}
data-l10n-id="turn-off-scheduled-backups-cancel-button"
></moz-button>
<moz-button

View File

@ -5,9 +5,29 @@
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-message-bar.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/backup/password-validation-inputs.mjs";
import { ERRORS } from "chrome://browser/content/backup/backup-constants.mjs";
const ENABLE_ERROR_L10N_IDS = Object.freeze({
[ERRORS.FILE_SYSTEM_ERROR]: "turn-on-scheduled-backups-error-file-system",
[ERRORS.INVALID_PASSWORD]: "backup-error-password-requirements",
[ERRORS.UNKNOWN]: "backup-error-retry",
});
/**
* @param {number} errorCode Error code from backup-constants.mjs
* @returns {string} Localization ID for error message
*/
function getEnableErrorL10nId(errorCode) {
return (
ENABLE_ERROR_L10N_IDS[errorCode] ?? ENABLE_ERROR_L10N_IDS[ERRORS.UNKNOWN]
);
}
/**
* The widget for showing available options when users want to turn on
* scheduled backups.
@ -16,16 +36,22 @@ export default class TurnOnScheduledBackups extends MozLitElement {
#placeholderIconURL = "chrome://global/skin/icons/page-portrait.svg";
static properties = {
// passed in from parents
defaultIconURL: { type: String, reflect: true },
defaultLabel: { type: String, reflect: true },
defaultPath: { type: String, reflect: true },
_newIconURL: { type: String },
_newLabel: { type: String },
_newPath: { type: String },
supportBaseLink: { type: String },
// internal state
_newIconURL: { type: String, state: true },
_newLabel: { type: String, state: true },
_newPath: { type: String, state: true },
_showPasswordOptions: { type: Boolean, reflect: true, state: true },
_passwordsMatch: { type: Boolean, state: true },
_inputPassValue: { type: String, state: true },
supportBaseLink: { type: String },
// managed by BackupUIChild
enableBackupErrorCode: { type: Number },
};
static get queries() {
@ -37,6 +63,7 @@ export default class TurnOnScheduledBackups extends MozLitElement {
filePathInputDefaultEl: "#backup-location-filepicker-input-default",
passwordOptionsCheckboxEl: "#sensitive-data-checkbox-input",
passwordOptionsExpandedEl: "#passwords",
errorEl: "#enable-backup-encryption-error",
};
}
@ -50,19 +77,19 @@ export default class TurnOnScheduledBackups extends MozLitElement {
this._newPath = "";
this._showPasswordOptions = false;
this._passwordsMatch = false;
this.enableBackupErrorCode = 0;
}
/**
* Dispatches the BackupUI:InitWidget custom event upon being attached to the
* DOM, which registers with BackupUIChild for BackupService state updates.
*/
connectedCallback() {
super.connectedCallback();
this.dispatchEvent(
new CustomEvent("BackupUI:InitWidget", { bubbles: true })
);
// listen to events from BackupUIChild
this.addEventListener("BackupUI:SelectNewFilepickerPath", this);
// listen to events from <password-validation-inputs>
this.addEventListener("ValidPasswordsDetected", this);
this.addEventListener("InvalidPasswordsDetected", this);
}
@ -94,14 +121,14 @@ export default class TurnOnScheduledBackups extends MozLitElement {
);
}
handleCancel() {
close() {
this.dispatchEvent(
new CustomEvent("dialogCancel", {
bubbles: true,
composed: true,
})
);
this.resetChanges();
this.reset();
}
handleConfirm() {
@ -114,13 +141,11 @@ export default class TurnOnScheduledBackups extends MozLitElement {
}
this.dispatchEvent(
new CustomEvent("turnOnScheduledBackups", {
new CustomEvent("BackupUI:EnableScheduledBackups", {
bubbles: true,
composed: true,
detail,
})
);
this.resetChanges();
}
handleTogglePasswordOptions() {
@ -128,7 +153,7 @@ export default class TurnOnScheduledBackups extends MozLitElement {
this._passwordsMatch = false;
}
resetChanges() {
reset() {
this._newPath = "";
this._newIconURL = "";
this._newLabel = "";
@ -136,11 +161,12 @@ export default class TurnOnScheduledBackups extends MozLitElement {
this.passwordOptionsCheckboxEl.checked = false;
this._passwordsMatch = false;
this._inputPassValue = "";
this.enableBackupErrorCode = 0;
if (this.passwordOptionsExpandedEl) {
this.passwordOptionsExpandedEl.dispatchEvent(
new CustomEvent("resetInputs", { bubbles: true, composed: true })
);
/** @type {import("./password-validation-inputs.mjs").default} */
const passwordElement = this.passwordOptionsExpandedEl;
passwordElement.reset();
}
}
@ -180,6 +206,16 @@ export default class TurnOnScheduledBackups extends MozLitElement {
`;
}
errorTemplate() {
return html`
<moz-message-bar
id="enable-backup-encryption-error"
type="error"
.messageL10nId="${getEnableErrorL10nId(this.enableBackupErrorCode)}"
></moz-message-bar>
`;
}
allOptionsTemplate() {
return html`
<fieldset id="all-controls">
@ -269,12 +305,13 @@ export default class TurnOnScheduledBackups extends MozLitElement {
></a>
</div>
${this.allOptionsTemplate()}
${this.enableBackupErrorCode ? this.errorTemplate() : null}
</main>
<moz-button-group id="backup-turn-on-scheduled-button-group">
<moz-button
id="backup-turn-on-scheduled-cancel-button"
@click=${this.handleCancel}
@click=${this.close}
data-l10n-id="turn-on-scheduled-backups-cancel-button"
></moz-button>
<moz-button

View File

@ -5,24 +5,43 @@
// eslint-disable-next-line import/no-unresolved
import { html, ifDefined } from "lit.all.mjs";
import "chrome://global/content/elements/moz-card.mjs";
import { ERRORS } from "chrome://browser/content/backup/backup-constants.mjs";
import "./turn-on-scheduled-backups.mjs";
window.MozXULElement.insertFTLIfNeeded("locales-preview/backupSettings.ftl");
window.MozXULElement.insertFTLIfNeeded("branding/brand.ftl");
const SELECTABLE_ERRORS = {
"(none)": 0,
...ERRORS,
};
export default {
title: "Domain-specific UI Widgets/Backup/Turn On Scheduled Backups",
component: "turn-on-scheduled-backups",
argTypes: {},
argTypes: {
enableBackupErrorCode: {
options: Object.keys(SELECTABLE_ERRORS),
mapping: SELECTABLE_ERRORS,
control: { type: "select" },
},
},
};
const Template = ({ defaultPath, _newPath, defaultLabel, _newLabel }) => html`
const Template = ({
defaultPath,
_newPath,
defaultLabel,
_newLabel,
enableBackupErrorCode,
}) => html`
<moz-card style="width: 27.8rem; position: relative;">
<turn-on-scheduled-backups
defaultPath=${defaultPath}
_newPath=${ifDefined(_newPath)}
defaultLabel=${defaultLabel}
_newLabel=${ifDefined(_newLabel)}
.enableBackupErrorCode=${enableBackupErrorCode}
></turn-on-scheduled-backups>
</moz-card>
`;
@ -39,3 +58,9 @@ CustomLocation.args = {
_newPath: "/Some/Test/Custom/Dir",
_newLabel: "Dir",
};
export const EnableError = Template.bind({});
EnableError.args = {
...CustomLocation.args,
enableBackupErrorCode: ERRORS.FILE_SYSTEM_ERROR,
};

View File

@ -99,7 +99,10 @@ add_task(async function test_disable_backup_encryption_confirm() {
);
let confirmButton = disableBackupEncryption.confirmButtonEl;
let promise = BrowserTestUtils.waitForEvent(window, "disableEncryption");
let promise = BrowserTestUtils.waitForEvent(
window,
"BackupUI:DisableEncryption"
);
Assert.ok(confirmButton, "Confirm button should be found");

View File

@ -91,7 +91,7 @@ add_task(async function test_enable_backup_encryption_checkbox_confirm() {
let encryptionPromise = BrowserTestUtils.waitForEvent(
window,
"enableEncryption"
"BackupUI:EnableEncryption"
);
confirmButton.click();
@ -185,7 +185,10 @@ add_task(
await settings.updateComplete;
confirmButton = settings.enableBackupEncryptionEl.confirmButtonEl;
let promise = BrowserTestUtils.waitForEvent(window, "rerunEncryption");
let promise = BrowserTestUtils.waitForEvent(
window,
"BackupUI:RerunEncryption"
);
confirmButton.click();
await promise;

View File

@ -37,7 +37,7 @@ async function turnOffScheduledBackupsHelper(browser, taskFn) {
let confirmButton = turnOffScheduledBackups.confirmButtonEl;
let promise = BrowserTestUtils.waitForEvent(
window,
"turnOffScheduledBackups"
"BackupUI:DisableScheduledBackups"
);
Assert.ok(confirmButton, "Confirm button should be found");

View File

@ -3,6 +3,10 @@
"use strict";
const { ERRORS } = ChromeUtils.importESModule(
"chrome://browser/content/backup/backup-constants.mjs"
);
const SCHEDULED_BACKUPS_ENABLED_PREF = "browser.backup.scheduled.enabled";
add_setup(async () => {
@ -43,7 +47,7 @@ add_task(async function test_turn_on_scheduled_backups_confirm() {
let confirmButton = turnOnScheduledBackups.confirmButtonEl;
let promise = BrowserTestUtils.waitForEvent(
window,
"turnOnScheduledBackups"
"BackupUI:EnableScheduledBackups"
);
Assert.ok(confirmButton, "Confirm button should be found");
@ -86,6 +90,7 @@ add_task(async function test_turn_on_custom_location_filepicker() {
MockFilePicker.returnValue = MockFilePicker.returnOK;
// After setting up mocks, start testing components
/** @type {import("../../content/backup-settings.mjs").default} */
let settings = browser.contentDocument.querySelector("backup-settings");
let turnOnButton = settings.scheduledBackupsButtonEl;
@ -148,7 +153,7 @@ add_task(async function test_turn_on_custom_location_filepicker() {
let confirmButtonPromise = BrowserTestUtils.waitForEvent(
window,
"turnOnScheduledBackups"
"BackupUI:EnableScheduledBackups"
);
confirmButton.click();
@ -234,7 +239,7 @@ add_task(async function test_turn_on_scheduled_backups_encryption() {
let promise = BrowserTestUtils.waitForEvent(
window,
"turnOnScheduledBackups"
"BackupUI:EnableScheduledBackups"
);
confirmButton.click();
@ -280,7 +285,7 @@ add_task(async function test_turn_on_scheduled_backups_encryption_error() {
let encryptionStub = sandbox
.stub(BackupService.prototype, "enableEncryption")
.throws();
.throws(new Error("test error", { cause: ERRORS.INVALID_PASSWORD }));
// Enable passwords
let passwordsCheckbox = turnOnScheduledBackups.passwordOptionsCheckboxEl;
@ -317,7 +322,7 @@ add_task(async function test_turn_on_scheduled_backups_encryption_error() {
let promise = BrowserTestUtils.waitForEvent(
window,
"turnOnScheduledBackups"
"BackupUI:EnableScheduledBackups"
);
confirmButton.click();
@ -339,6 +344,16 @@ add_task(async function test_turn_on_scheduled_backups_encryption_error() {
"Scheduled backups pref should still be false"
);
await BrowserTestUtils.waitForCondition(
() => !!turnOnScheduledBackups.errorEl,
"Error should be displayed to the user"
);
Assert.ok(
turnOnScheduledBackups.errorEl,
"Error should be displayed to the user"
);
sandbox.restore();
Services.prefs.clearUserPref(SCHEDULED_BACKUPS_ENABLED_PREF);
});

View File

@ -9,6 +9,7 @@ const { BackupService } = ChromeUtils.importESModule(
const { MockFilePicker } = SpecialPowers;
/** @type {{sinon: import("@types/sinon").SinonApi}} */
const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);

View File

@ -16,22 +16,6 @@
"resource://testing-common/BrowserTestUtils.sys.mjs"
);
/**
* Tests that adding a disable-backup-encryption element to the DOM causes it to
* fire a BackupUI:InitWidget event.
*/
add_task(async function test_initWidget() {
let disableBackupEncryption = document.createElement("disable-backup-encryption");
let content = document.getElementById("content");
let sawInitWidget = BrowserTestUtils.waitForEvent(content, "BackupUI:InitWidget");
content.appendChild(disableBackupEncryption);
await sawInitWidget;
ok(true, "Saw BackupUI:InitWidget");
disableBackupEncryption.remove();
});
/**
* Tests that pressing the confirm button will dispatch the expected events.
*/
@ -42,7 +26,7 @@
ok(confirmButton, "Confirm button should be found");
let content = document.getElementById("content");
let promise = BrowserTestUtils.waitForEvent(content, "disableEncryption");
let promise = BrowserTestUtils.waitForEvent(content, "BackupUI:DisableEncryption");
confirmButton.click()

View File

@ -17,22 +17,6 @@
"resource://testing-common/BrowserTestUtils.sys.mjs"
);
/**
* Tests that adding a enable-backup-encryption element to the DOM causes it to
* fire a BackupUI:InitWidget event.
*/
add_task(async function test_initWidget() {
let enableBackupEncryption = document.createElement("enable-backup-encryption");
let content = document.getElementById("content");
let sawInitWidget = BrowserTestUtils.waitForEvent(content, "BackupUI:InitWidget");
content.appendChild(enableBackupEncryption);
await sawInitWidget;
ok(true, "Saw BackupUI:InitWidget");
enableBackupEncryption.remove();
});
/**
* Tests that pressing the confirm button for the set-password type dialog will dispatch the expected events.
*/
@ -61,12 +45,14 @@
ok(!confirmButton.disabled, "Confirm button should no longer be disabled");
let content = document.getElementById("content");
let encryptionPromise = BrowserTestUtils.waitForEvent(content, "enableEncryption");
let encryptionPromise = BrowserTestUtils.waitForEvent(content, "BackupUI:EnableEncryption");
confirmButton.click()
await encryptionPromise;
ok(true, "Detected event after selecting the confirm button");
enableBackupEncryption.reset();
})
/**
@ -99,12 +85,14 @@
ok(!confirmButton.disabled, "Confirm button should no longer be disabled");
let content = document.getElementById("content");
let encryptionPromise = BrowserTestUtils.waitForEvent(content, "rerunEncryption");
let encryptionPromise = BrowserTestUtils.waitForEvent(content, "BackupUI:RerunEncryption");
confirmButton.click()
await encryptionPromise;
ok(true, "Detected event after selecting the confirm button");
enableBackupEncryption.reset();
})
/**
@ -126,6 +114,8 @@
await promise;
ok(true, "Detected event after selecting the cancel button");
enableBackupEncryption.reset();
})
</script>
</head>

View File

@ -16,22 +16,6 @@
"resource://testing-common/BrowserTestUtils.sys.mjs"
);
/**
* Tests that adding a turn-off-scheduled-backups element to the DOM causes it to
* fire a BackupUI:InitWidget event.
*/
add_task(async function test_initWidget() {
let turnOffScheduledBackups = document.createElement("turn-off-scheduled-backups");
let content = document.getElementById("content");
let sawInitWidget = BrowserTestUtils.waitForEvent(content, "BackupUI:InitWidget");
content.appendChild(turnOffScheduledBackups);
await sawInitWidget;
ok(true, "Saw BackupUI:InitWidget");
turnOffScheduledBackups.remove();
});
/**
* Tests that pressing the confirm button will dispatch the expected events.
*/
@ -42,7 +26,7 @@
ok(confirmButton, "Confirm button should be found");
let content = document.getElementById("content");
let promise = BrowserTestUtils.waitForEvent(content, "turnOffScheduledBackups");
let promise = BrowserTestUtils.waitForEvent(content, "BackupUI:DisableScheduledBackups");
confirmButton.click()

View File

@ -45,7 +45,7 @@
ok(confirmButton, "Confirm button should be found");
let content = document.getElementById("content");
let promise = BrowserTestUtils.waitForEvent(content, "turnOnScheduledBackups");
let promise = BrowserTestUtils.waitForEvent(content, "BackupUI:EnableScheduledBackups");
confirmButton.click()

View File

@ -81,6 +81,10 @@ turn-on-scheduled-backups-encryption-repeat-password-label = Repeat password
turn-on-scheduled-backups-cancel-button = Cancel
turn-on-scheduled-backups-confirm-button = Turn on backup
# Tell the user there was an error accessing the user's selected backup
# folder. The folder may be invalid or inaccessible.
turn-on-scheduled-backups-error-file-system = There was a problem with your selected backup folder. Choose a different folder and try again.
## These strings are displayed in a modal when users want to turn off scheduled backups.
turn-off-scheduled-backups-header = Turn off backup?
@ -195,6 +199,17 @@ disable-backup-encryption-support-link = What will be backed up?
disable-backup-encryption-cancel-button = Cancel
disable-backup-encryption-confirm-button = Remove password
## These strings are used to tell users when errors occur when using
## the backup system
backup-error-password-requirements = Your password doesnt meet the requirements. Please try another password.
# This error message will be shown to the user when something went wrong with
# the backup system but we do not have any more specific idea of what went
# wrong. This message invites the user to try an action again because there
# is a chance that the action will succeed if retried.
backup-error-retry = Something went wrong. Please try again.
## These strings are inserted into the generated single-file backup archive.
## The single-file backup archive is a specially-crafted, static HTML file
## that is placed within a user specified directory (the Documents folder by