commit e567fc04567b9f5115f0fb7f75d4bf34a3d1ceac
parent a69b0adb399ce162b471b284e3010758f3d146b6
Author: Harsheet <hsohaney@mozilla.com>
Date: Sun, 16 Nov 2025 21:14:13 +0000
Bug 1999786 - Save a persistent state between screens for the backup spotlight. r=cdupuis,omc-reviewers,emcminn,sthompson
Differential Revision: https://phabricator.services.mozilla.com/D272543
Diffstat:
5 files changed, 261 insertions(+), 26 deletions(-)
diff --git a/browser/components/DesktopActorRegistry.sys.mjs b/browser/components/DesktopActorRegistry.sys.mjs
@@ -239,6 +239,10 @@ let JSWINDOWACTORS = {
"BackupUI:ShowBackupLocation": { wantUntrusted: true },
"BackupUI:EditBackupLocation": { wantUntrusted: true },
"BackupUI:ErrorBarDismissed": { wantUntrusted: true },
+ "BackupUI:SetEmbeddedComponentPersistentData": { wantUntrusted: true },
+ "BackupUI:FlushEmbeddedComponentPersistentData": {
+ wantUntrusted: true,
+ },
},
},
includeChrome: true,
diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs
@@ -775,6 +775,19 @@ export class BackupService extends EventTarget {
}
/**
+ * Sets the persisted options between screens for embedded components.
+ * This is specifically used in the Spotlight onboarding experience.
+ *
+ * This data is flushed upon creating a backup or exiting the backup flow.
+ *
+ * @param {object} data - data to persist between screens.
+ */
+ setEmbeddedComponentPersistentData(data) {
+ this.#_state.embeddedComponentPersistentData = { ...data };
+ this.stateUpdate();
+ }
+
+ /**
* An object holding the current state of the BackupService instance, for
* the purposes of representing it in the user interface. Ideally, this would
* be named #state instead of #_state, but sphinx-js seems to be fairly
@@ -807,6 +820,8 @@ export class BackupService extends EventTarget {
backupErrorCode: lazy.backupErrorCode,
archiveEnabledStatus: this.archiveEnabledStatus.enabled,
restoreEnabledStatus: this.restoreEnabledStatus.enabled,
+ /** Utilized by the spotlight to persist information between screens */
+ embeddedComponentPersistentData: {},
};
/**
@@ -3591,6 +3606,9 @@ export class BackupService extends EventTarget {
if (shouldEnableScheduledBackups) {
// reset the error states when reenabling backup
Services.prefs.setIntPref(BACKUP_ERROR_CODE_PREF_NAME, ERRORS.NONE);
+
+ // flush the embedded component's persistent data
+ this.setEmbeddedComponentPersistentData({});
} else {
// set user-disabled pref if backup is being disabled
Services.prefs.setBoolPref(
diff --git a/browser/components/backup/actors/BackupUIChild.sys.mjs b/browser/components/backup/actors/BackupUIChild.sys.mjs
@@ -140,6 +140,10 @@ export class BackupUIChild extends JSWindowActorChild {
this.sendAsyncMessage("EditBackupLocation");
} else if (event.type == "BackupUI:ErrorBarDismissed") {
this.sendAsyncMessage("ErrorBarDismissed");
+ } else if (event.type == "BackupUI:SetEmbeddedComponentPersistentData") {
+ this.sendAsyncMessage("SetEmbeddedComponentPersistentData", event.detail);
+ } else if (event.type == "BackupUI:FlushEmbeddedComponentPersistentData") {
+ this.sendAsyncMessage("FlushEmbeddedComponentPersistentData");
}
}
diff --git a/browser/components/backup/content/turn-on-scheduled-backups.mjs b/browser/components/backup/content/turn-on-scheduled-backups.mjs
@@ -151,6 +151,20 @@ export default class TurnOnScheduledBackups extends MozLitElement {
this._newPath = path;
this._newLabel = filename;
this._newIconURL = iconURL;
+
+ if (this.embeddedFxBackupOptIn) {
+ // Let's set a persistent path
+ this.dispatchEvent(
+ new CustomEvent("BackupUI:SetEmbeddedComponentPersistentData", {
+ bubbles: true,
+ detail: {
+ path,
+ label: filename,
+ iconURL,
+ },
+ })
+ );
+ }
} else if (event.type == "ValidPasswordsDetected") {
let { password } = event.detail;
this._passwordsMatch = true;
@@ -199,17 +213,22 @@ export default class TurnOnScheduledBackups extends MozLitElement {
detail.password = this._inputPassValue;
}
- if (
- this.embeddedFxBackupOptIn &&
- this.backupIsEncrypted &&
- !detail.password
- ) {
- // We're in the embedded component and we haven't set a password yet when
- // one is expected, let's not do a confirm action yet!
- this.dispatchEvent(
- new CustomEvent("SpotlightOnboardingAdvanceScreens", { bubbles: true })
- );
- return;
+ if (this.embeddedFxBackupOptIn && this.backupIsEncrypted) {
+ if (!detail.password) {
+ // We're in the embedded component and we haven't set a password yet
+ // when one is expected, let's not do a confirm action yet!
+ this.dispatchEvent(
+ new CustomEvent("SpotlightOnboardingAdvanceScreens", {
+ bubbles: true,
+ })
+ );
+ return;
+ }
+
+ // The persistent data will take precedence over the default path
+ detail.parentDirPath =
+ this.backupServiceState?.embeddedComponentPersistentData?.path ||
+ detail.parentDirPath;
}
this.dispatchEvent(
@@ -258,6 +277,17 @@ export default class TurnOnScheduledBackups extends MozLitElement {
const passwordElement = this.passwordOptionsExpandedEl;
passwordElement.reset();
}
+
+ if (
+ this.embeddedFxBackupOptIn &&
+ this.backupServiceState?.embeddedComponentPersistentData
+ ) {
+ this.dispatchEvent(
+ new CustomEvent("BackupUI:FlushEmbeddedComponentPersistentData", {
+ bubbles: true,
+ })
+ );
+ }
}
defaultFilePathInputTemplate() {
@@ -285,9 +315,19 @@ export default class TurnOnScheduledBackups extends MozLitElement {
`;
}
+ /**
+ * Note: We also consider the embeddedComponentPersistentData since we might be in the
+ * Spotlight where we need this persistent data between screens. This state property should
+ * not be set if we are not in the Spotlight.
+ */
customFilePathInputTemplate() {
- let filename = this._newLabel;
- let iconURL = this._newIconURL || this.#placeholderIconURL;
+ let filename =
+ this._newLabel ||
+ this.backupServiceState?.embeddedComponentPersistentData?.label;
+ let iconURL =
+ this._newIconURL ||
+ this.backupServiceState?.embeddedComponentPersistentData?.iconURL ||
+ this.#placeholderIconURL;
return html`
<input
@@ -322,7 +362,8 @@ export default class TurnOnScheduledBackups extends MozLitElement {
"turn-on-scheduled-backups-location-label"}
></label>
<div id="backup-location-filepicker">
- ${!this._newPath
+ ${!this._newPath &&
+ !this.backupServiceState?.embeddedComponentPersistentData?.path
? this.defaultFilePathInputTemplate()
: this.customFilePathInputTemplate()}
<moz-button
diff --git a/browser/components/backup/tests/browser/browser_settings_turn_on_scheduled_backups.js b/browser/components/backup/tests/browser/browser_settings_turn_on_scheduled_backups.js
@@ -9,6 +9,22 @@ const { ERRORS } = ChromeUtils.importESModule(
const SCHEDULED_BACKUPS_ENABLED_PREF = "browser.backup.scheduled.enabled";
+async function setup_mockFilePicker(mockParentDir) {
+ const dummyFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+
+ dummyFile.initWithPath(mockParentDir);
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ Assert.ok(true, "Filepicker shown");
+ MockFilePicker.setFiles([dummyFile]);
+ resolve();
+ };
+ });
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ return { filePickerShownPromise };
+}
+
add_setup(async () => {
MockFilePicker.init(window.browsingContext);
registerCleanupFunction(() => {
@@ -96,19 +112,9 @@ add_task(async function test_turn_on_custom_location_filepicker() {
PathUtils.tempDir,
"settings-custom-dir-test"
);
- const dummyFile = Cc["@mozilla.org/file/local;1"].createInstance(
- Ci.nsIFile
- );
- dummyFile.initWithPath(mockCustomParentDir);
- let filePickerShownPromise = new Promise(resolve => {
- MockFilePicker.showCallback = () => {
- Assert.ok(true, "Filepicker shown");
- MockFilePicker.setFiles([dummyFile]);
- resolve();
- };
- });
- MockFilePicker.returnValue = MockFilePicker.returnOK;
+ let { filePickerShownPromise } =
+ await setup_mockFilePicker(mockCustomParentDir);
// After setting up mocks, start testing components
/** @type {import("../../content/backup-settings.mjs").default} */
@@ -549,3 +555,165 @@ add_task(async function test_default_location_selected() {
await SpecialPowers.popPrefEnv();
});
+
+/**
+ * Tests that the persistent data for embedded components is set when a user picks a file
+ * and is flushed once backup is enabled.
+ */
+add_task(async function test_embedded_component_persistent_data_filepicker() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SCHEDULED_BACKUPS_ENABLED_PREF, false]],
+ });
+
+ await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
+ const mockCustomParentDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "our-dummy-folder"
+ );
+ let { filePickerShownPromise } =
+ await setup_mockFilePicker(mockCustomParentDir);
+
+ let settings = browser.contentDocument.querySelector("backup-settings");
+ let turnOnButton = settings.scheduledBackupsButtonEl;
+
+ Assert.ok(
+ turnOnButton,
+ "Button to turn on scheduled backups should be found"
+ );
+
+ turnOnButton.click();
+
+ await settings.updateComplete;
+ let turnOnScheduledBackups = settings.turnOnScheduledBackupsEl;
+
+ // for the purposes of this test, we will act like we are an embedded component
+ turnOnScheduledBackups.embeddedFxBackupOptIn = true;
+
+ // First verify the default input value and dir path button
+ let filePathButton = turnOnScheduledBackups.filePathButtonEl;
+ let stateUpdatePromise = BrowserTestUtils.waitForEvent(
+ window,
+ "BackupUI:StateWasUpdated"
+ );
+ Assert.ok(
+ filePathButton,
+ "Button for choosing a file path should be found"
+ );
+ filePathButton.click();
+
+ await filePickerShownPromise;
+ await stateUpdatePromise;
+ await turnOnScheduledBackups.updateComplete;
+
+ Assert.equal(
+ settings.backupServiceState.embeddedComponentPersistentData.path,
+ mockCustomParentDir,
+ "Our persistent path should be set correctly"
+ );
+
+ // Let's create a backup
+ let confirmButton = turnOnScheduledBackups.confirmButtonEl;
+ let promise = BrowserTestUtils.waitForEvent(
+ window,
+ "BackupUI:EnableScheduledBackups"
+ );
+
+ Assert.ok(confirmButton, "Confirm button should be found");
+
+ confirmButton.click();
+
+ await promise;
+ await settings.updateComplete;
+ });
+
+ await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => {
+ let settings = browser.contentDocument.querySelector("backup-settings");
+ await settings.updateComplete;
+
+ Assert.deepEqual(
+ settings.backupServiceState.embeddedComponentPersistentData,
+ {},
+ "Our persistent path should be flushed"
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that the persistent data for embedded components is flushed if the dialog is cancelled
+ */
+add_task(
+ async function test_embedded_component_persistent_data_filepicker_cancelled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SCHEDULED_BACKUPS_ENABLED_PREF, false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ "about:preferences#sync",
+ async browser => {
+ const mockCustomParentDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "our-dummy-folder"
+ );
+ let { filePickerShownPromise } =
+ await setup_mockFilePicker(mockCustomParentDir);
+
+ let settings = browser.contentDocument.querySelector("backup-settings");
+ let turnOnButton = settings.scheduledBackupsButtonEl;
+
+ Assert.ok(
+ turnOnButton,
+ "Button to turn on scheduled backups should be found"
+ );
+
+ turnOnButton.click();
+
+ await settings.updateComplete;
+ let turnOnScheduledBackups = settings.turnOnScheduledBackupsEl;
+
+ // for the purposes of this test, we will act like we are an embedded component
+ turnOnScheduledBackups.embeddedFxBackupOptIn = true;
+
+ // First verify the default input value and dir path button
+ let filePathButton = turnOnScheduledBackups.filePathButtonEl;
+ const waitForStateUpdate = () =>
+ BrowserTestUtils.waitForEvent(window, "BackupUI:StateWasUpdated");
+
+ let stateUpdatePromise = waitForStateUpdate();
+
+ Assert.ok(
+ filePathButton,
+ "Button for choosing a file path should be found"
+ );
+ filePathButton.click();
+
+ await filePickerShownPromise;
+ await stateUpdatePromise;
+ await turnOnScheduledBackups.updateComplete;
+
+ Assert.equal(
+ settings.backupServiceState.embeddedComponentPersistentData.path,
+ mockCustomParentDir,
+ "Our persistent path should be set correctly"
+ );
+
+ stateUpdatePromise = waitForStateUpdate();
+
+ let dialog = settings.turnOnScheduledBackupsDialogEl;
+ let closedPromise = BrowserTestUtils.waitForEvent(dialog, "close");
+ dialog.close();
+ await closedPromise;
+ await stateUpdatePromise;
+
+ Assert.deepEqual(
+ settings.backupServiceState.embeddedComponentPersistentData,
+ {},
+ "Our persistent path should be flushed"
+ );
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+ }
+);