diff options
Diffstat (limited to 'browser')
1131 files changed, 23847 insertions, 13198 deletions
diff --git a/browser/actors/AboutReaderParent.sys.mjs b/browser/actors/AboutReaderParent.sys.mjs index 544a257cbc..00126910ae 100644 --- a/browser/actors/AboutReaderParent.sys.mjs +++ b/browser/actors/AboutReaderParent.sys.mjs @@ -281,7 +281,7 @@ export class AboutReaderParent extends JSWindowActorParent { * @return {Promise} * @resolves JS object representing the article, or null if no article is found. */ - async _getArticle(url, browser) { + async _getArticle(url) { return lazy.ReaderMode.downloadAndParseDocument(url).catch(e => { if (e && e.newURL) { // Pass up the error so we can navigate the browser in question to the new URL: diff --git a/browser/actors/ContextMenuChild.sys.mjs b/browser/actors/ContextMenuChild.sys.mjs index e16efdc9cd..5ecbcb22e9 100644 --- a/browser/actors/ContextMenuChild.sys.mjs +++ b/browser/actors/ContextMenuChild.sys.mjs @@ -371,7 +371,7 @@ export class ContextMenuChild extends JSWindowActorChild { } // Returns true if clicked-on link targets a resource that can be saved. - _isLinkSaveable(aLink) { + _isLinkSaveable() { // We don't do the Right Thing for news/snews yet, so turn them off // until we do. return ( @@ -696,7 +696,7 @@ export class ContextMenuChild extends JSWindowActorChild { * - link * - linkURI */ - _cleanContext(aEvent) { + _cleanContext() { const context = this.context; const cleanTarget = Object.create(null); diff --git a/browser/actors/FormValidationChild.sys.mjs b/browser/actors/FormValidationChild.sys.mjs index f5ce427d03..bb67f1f1f4 100644 --- a/browser/actors/FormValidationChild.sys.mjs +++ b/browser/actors/FormValidationChild.sys.mjs @@ -138,7 +138,7 @@ export class FormValidationChild extends JSWindowActorChild { * Blur event handler in which we disconnect from the form element and * hide the popup. */ - _onBlur(aEvent) { + _onBlur() { if (this._element) { this._element.removeEventListener("input", this); this._element.removeEventListener("blur", this); diff --git a/browser/actors/FormValidationParent.sys.mjs b/browser/actors/FormValidationParent.sys.mjs index e95a8e86fb..a988b06f37 100644 --- a/browser/actors/FormValidationParent.sys.mjs +++ b/browser/actors/FormValidationParent.sys.mjs @@ -19,7 +19,7 @@ class PopupShownObserver { this._weakContext = Cu.getWeakReference(browsingContext); } - observe(subject, topic, data) { + observe(subject, topic) { let ctxt = this._weakContext.get(); let actor = ctxt.currentWindowGlobal?.getExistingActor("FormValidation"); if (!actor) { diff --git a/browser/actors/PluginParent.sys.mjs b/browser/actors/PluginParent.sys.mjs index fa93c1d5ab..14eeb38945 100644 --- a/browser/actors/PluginParent.sys.mjs +++ b/browser/actors/PluginParent.sys.mjs @@ -19,7 +19,7 @@ ChromeUtils.defineLazyGetter(lazy, "gNavigatorBundle", function () { export const PluginManager = { gmpCrashes: new Map(), - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "gmp-plugin-crash": this._registerGMPCrash(subject); diff --git a/browser/actors/PromptParent.sys.mjs b/browser/actors/PromptParent.sys.mjs index 4a159cbda5..83180923b9 100644 --- a/browser/actors/PromptParent.sys.mjs +++ b/browser/actors/PromptParent.sys.mjs @@ -9,42 +9,22 @@ ChromeUtils.defineESModuleGetters(lazy, { PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs", BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", }); -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; - -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "tabChromePromptSubDialog", - "prompts.tabChromePromptSubDialog", - false -); - -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "contentPromptSubDialog", - "prompts.contentPromptSubDialog", - false -); ChromeUtils.defineLazyGetter(lazy, "gTabBrowserLocalization", function () { return new Localization(["browser/tabbrowser.ftl"], true); }); /** - * @typedef {Object} Prompt - * @property {Function} resolver - * The resolve function to be called with the data from the Prompt - * after the user closes it. - * @property {Object} tabModalPrompt - * The TabModalPrompt being shown to the user. + * @typedef {Object} Dialog */ /** - * gBrowserPrompts weakly maps BrowsingContexts to a Map of their currently - * active Prompts. + * gBrowserDialogs weakly maps BrowsingContexts to a Map of their currently + * active Dialogs. * - * @type {WeakMap<BrowsingContext, Prompt>} + * @type {WeakMap<BrowsingContext, Dialog>} */ -let gBrowserPrompts = new WeakMap(); +let gBrowserDialogs = new WeakMap(); export class PromptParent extends JSWindowActorParent { didDestroy() { @@ -54,35 +34,24 @@ export class PromptParent extends JSWindowActorParent { } /** - * Registers a new Prompt to be tracked for a particular BrowsingContext. - * We need to track a Prompt so that we can, for example, force-close the - * TabModalPrompt if the originating subframe or tab unloads or crashes. + * Registers a new dialog to be tracked for a particular BrowsingContext. + * We need to track a dialog so that we can, for example, force-close the + * dialog if the originating subframe or tab unloads or crashes. * - * @param {Object} tabModalPrompt - * The TabModalPrompt that will be shown to the user. + * @param {Dialog} dialog + * The dialog that will be shown to the user. * @param {string} id - * A unique ID to differentiate multiple Prompts coming from the same + * A unique ID to differentiate multiple dialogs coming from the same * BrowsingContext. - * @return {Promise} - * @resolves {Object} - * Resolves with the arguments returned from the TabModalPrompt when it - * is dismissed. */ - registerPrompt(tabModalPrompt, id) { - let prompts = gBrowserPrompts.get(this.browsingContext); - if (!prompts) { - prompts = new Map(); - gBrowserPrompts.set(this.browsingContext, prompts); + registerDialog(dialog, id) { + let dialogs = gBrowserDialogs.get(this.browsingContext); + if (!dialogs) { + dialogs = new Map(); + gBrowserDialogs.set(this.browsingContext, dialogs); } - let promise = new Promise(resolve => { - prompts.set(id, { - tabModalPrompt, - resolver: resolve, - }); - }); - - return promise; + dialogs.set(id, dialog); } /** @@ -94,20 +63,18 @@ export class PromptParent extends JSWindowActorParent { * BrowsingContext. */ unregisterPrompt(id) { - let prompts = gBrowserPrompts.get(this.browsingContext); - if (prompts) { - prompts.delete(id); - } + let dialogs = gBrowserDialogs.get(this.browsingContext); + dialogs?.delete(id); } /** * Programmatically closes all Prompts for the current BrowsingContext. */ forceClosePrompts() { - let prompts = gBrowserPrompts.get(this.browsingContext) || []; + let dialogs = gBrowserDialogs.get(this.browsingContext) || []; - for (let [, prompt] of prompts) { - prompt.tabModalPrompt && prompt.tabModalPrompt.abortPrompt(); + for (let [, dialog] of dialogs) { + dialog?.abort(); } } @@ -127,120 +94,19 @@ export class PromptParent extends JSWindowActorParent { } receiveMessage(message) { - let args = message.data; - let id = args._remoteId; - switch (message.name) { case "Prompt:Open": if (!this.windowContext.isActiveInTab) { return undefined; } - if ( - (args.modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT && - !lazy.contentPromptSubDialog) || - (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB && - !lazy.tabChromePromptSubDialog) - ) { - return this.openContentPrompt(args, id); - } - return this.openPromptWithTabDialogBox(args); + return this.openPromptWithTabDialogBox(message.data); } return undefined; } /** - * Opens a TabModalPrompt for a BrowsingContext, and puts the associated browser - * in the modal state until the TabModalPrompt is closed. - * - * @param {Object} args - * The arguments passed up from the BrowsingContext to be passed directly - * to the TabModalPrompt. - * @param {string} id - * A unique ID to differentiate multiple Prompts coming from the same - * BrowsingContext. - * @return {Promise} - * Resolves when the TabModalPrompt is dismissed. - * @resolves {Object} - * The arguments returned from the TabModalPrompt. - */ - openContentPrompt(args, id) { - let browser = this.browsingContext.top.embedderElement; - if (!browser) { - throw new Error("Cannot tab-prompt without a browser!"); - } - let window = browser.ownerGlobal; - let tabPrompt = window.gBrowser.getTabModalPromptBox(browser); - let newPrompt; - let needRemove = false; - - // If the page which called the prompt is different from the the top context - // where we show the prompt, ask the prompt implementation to display the origin. - // For example, this can happen if a cross origin subframe shows a prompt. - args.showCallerOrigin = - args.promptPrincipal && - !browser.contentPrincipal.equals(args.promptPrincipal); - - let onPromptClose = () => { - let promptData = gBrowserPrompts.get(this.browsingContext); - if (!promptData || !promptData.has(id)) { - throw new Error( - "Failed to close a prompt since it wasn't registered for some reason." - ); - } - - let { resolver, tabModalPrompt } = promptData.get(id); - // It's possible that we removed the prompt during the - // appendPrompt call below. In that case, newPrompt will be - // undefined. We set the needRemove flag to remember to remove - // it right after we've finished adding it. - if (tabModalPrompt) { - tabPrompt.removePrompt(tabModalPrompt); - } else { - needRemove = true; - } - - this.unregisterPrompt(id); - - lazy.PromptUtils.fireDialogEvent( - window, - "DOMModalDialogClosed", - browser, - this.getClosingEventDetail(args) - ); - resolver(args); - browser.maybeLeaveModalState(); - }; - - try { - browser.enterModalState(); - lazy.PromptUtils.fireDialogEvent( - window, - "DOMWillOpenModalDialog", - browser, - this.getOpenEventDetail(args) - ); - - args.promptActive = true; - - newPrompt = tabPrompt.appendPrompt(args, onPromptClose); - let promise = this.registerPrompt(newPrompt, id); - - if (needRemove) { - tabPrompt.removePrompt(newPrompt); - } - - return promise; - } catch (ex) { - console.error(ex); - onPromptClose(true); - } - - return null; - } - - /** * Opens either a window prompt or TabDialogBox at the content or tab level * for a BrowsingContext, and puts the associated browser in the modal state * until the prompt is closed. @@ -349,8 +215,9 @@ export class PromptParent extends JSWindowActorParent { ); } bag = lazy.PromptUtils.objectToPropBag(args); + let promptID = args._remoteId; try { - await dialogBox.open( + let { dialog, closedPromise } = dialogBox.open( uri, { features: "resizable=no", @@ -359,7 +226,9 @@ export class PromptParent extends JSWindowActorParent { hideContent: args.isTopLevelCrossDomainAuth, }, bag - ).closedPromise; + ); + this.registerDialog(dialog, promptID); + await closedPromise; } finally { if (args.isTopLevelCrossDomainAuth) { browser.currentAuthPromptURI = null; @@ -373,6 +242,7 @@ export class PromptParent extends JSWindowActorParent { currentLocationsTabLabel ); } + this.unregisterPrompt(promptID); } } else { // Ensure we set the correct modal type at this point. diff --git a/browser/actors/RefreshBlockerChild.sys.mjs b/browser/actors/RefreshBlockerChild.sys.mjs index 6ba63298b1..f5e9611144 100644 --- a/browser/actors/RefreshBlockerChild.sys.mjs +++ b/browser/actors/RefreshBlockerChild.sys.mjs @@ -50,7 +50,7 @@ var progressListener = { * the STATE_IS_WINDOW case, which will clear any mappings from * blockedWindows. */ - onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aWebProgress, aRequest, aStateFlags) { if ( aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW && aStateFlags & Ci.nsIWebProgressListener.STATE_STOP @@ -64,7 +64,7 @@ var progressListener = { * onRefreshAttempted has already fired for this DOM Window, will * send the appropriate refresh blocked data to the parent. */ - onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + onLocationChange(aWebProgress) { let win = aWebProgress.DOMWindow; if (this.blockedWindows.has(win)) { let data = this.blockedWindows.get(win); @@ -180,7 +180,7 @@ export class RefreshBlockerObserverChild extends JSProcessActorChild { this.filtersMap = new Map(); } - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "webnavigation-create": case "chrome-webnavigation-create": diff --git a/browser/actors/SearchSERPTelemetryChild.sys.mjs b/browser/actors/SearchSERPTelemetryChild.sys.mjs index c760f9a19e..e845f589b0 100644 --- a/browser/actors/SearchSERPTelemetryChild.sys.mjs +++ b/browser/actors/SearchSERPTelemetryChild.sys.mjs @@ -1077,7 +1077,11 @@ class DomainExtractor { break; } case "textContent": { - this.#fromElementsRetrieveTextContent(elements, extractedDomains); + this.#fromElementsRetrieveTextContent( + elements, + extractedDomains, + providerName + ); break; } } @@ -1197,8 +1201,26 @@ class DomainExtractor { * A list of elements from the page whose text content we want to inspect. * @param {Set<string>} extractedDomains * The result set of domains extracted from the page. + * @param {string} providerName + * The name of the search provider. */ - #fromElementsRetrieveTextContent(elements, extractedDomains) { + #fromElementsRetrieveTextContent(elements, extractedDomains, providerName) { + // Not an exhaustive regex, but it fits our purpose for this method. + const LOOSE_URL_REGEX = + /^(?:https?:\/\/)?(?:www\.)?(?:[\w\-]+\.)+(?:[\w\-]{2,})/i; + + // Known but acceptable limitations to this function, where the return + // value won't be correctly fixed up: + // 1) A url is embedded within other text. Ex: "xkcd.com is cool." + // 2) The url contains legal but unusual characters. Ex: $ ! * ' + function fixup(textContent) { + return textContent + .toLowerCase() + .replaceAll(" ", "") + .replace(/\.$/, "") + .concat(".com"); + } + for (let element of elements) { if (this.#exceedsThreshold(extractedDomains.size)) { return; @@ -1209,18 +1231,24 @@ class DomainExtractor { } let domain; - try { - domain = new URL(textContent).hostname; - } catch (e) { - domain = textContent.toLowerCase().replaceAll(" ", ""); - // If the attempt to turn the text content into a URL object only fails - // because we're missing a protocol, ".com" may already be present. - if (!domain.endsWith(".com")) { - domain = domain.concat(".com"); + if (LOOSE_URL_REGEX.test(textContent)) { + // Creating a new URL object will throw if the protocol is missing. + if (!/^https?:\/\//.test(textContent)) { + textContent = "https://" + textContent; + } + + try { + domain = new URL(textContent).hostname; + } catch (e) { + domain = fixup(textContent); } + } else { + domain = fixup(textContent); } - if (!extractedDomains.has(domain)) { - extractedDomains.add(domain); + + let processedDomain = this.#processDomain(domain, providerName); + if (processedDomain && !extractedDomains.has(processedDomain)) { + extractedDomains.add(processedDomain); } } } diff --git a/browser/actors/WebRTCChild.sys.mjs b/browser/actors/WebRTCChild.sys.mjs index 50db01709d..03ad6d389b 100644 --- a/browser/actors/WebRTCChild.sys.mjs +++ b/browser/actors/WebRTCChild.sys.mjs @@ -213,7 +213,7 @@ function getActorForWindow(window) { return null; } -function handlePCRequest(aSubject, aTopic, aData) { +function handlePCRequest(aSubject) { let { windowID, innerWindowID, callID, isSecure } = aSubject; let contentWindow = Services.wm.getOuterWindowWithId(windowID); if (!contentWindow.pendingPeerConnectionRequests) { @@ -235,7 +235,7 @@ function handlePCRequest(aSubject, aTopic, aData) { } } -function handleGUMStop(aSubject, aTopic, aData) { +function handleGUMStop(aSubject) { let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID); let request = { @@ -250,7 +250,7 @@ function handleGUMStop(aSubject, aTopic, aData) { } } -function handleGUMRequest(aSubject, aTopic, aData) { +function handleGUMRequest(aSubject) { // Now that a getUserMedia request has been created, we should check // to see if we're supposed to have any devices muted. This needs // to occur after the getUserMedia request is made, since the global @@ -472,7 +472,7 @@ function forgetPendingListsEventually(aContentWindow) { aContentWindow.removeEventListener("unload", WebRTCChild.handleEvent); } -function updateIndicators(aSubject, aTopic, aData) { +function updateIndicators(aSubject) { if ( aSubject instanceof Ci.nsIPropertyBag && aSubject.getProperty("requestURL") == kBrowserURL diff --git a/browser/actors/WebRTCParent.sys.mjs b/browser/actors/WebRTCParent.sys.mjs index 09c39e7393..806fd4abcf 100644 --- a/browser/actors/WebRTCParent.sys.mjs +++ b/browser/actors/WebRTCParent.sys.mjs @@ -609,7 +609,7 @@ function prompt(aActor, aBrowser, aRequest) { actionL10nIds.push({ id }, { id: "webrtc-action-always-block" }); secondaryActions = [ { - callback(aState) { + callback() { aActor.denyRequest(aRequest); if (!isNotNowLabelEnabled) { lazy.SitePermissions.setForPrincipal( @@ -623,7 +623,7 @@ function prompt(aActor, aBrowser, aRequest) { }, }, { - callback(aState) { + callback() { aActor.denyRequest(aRequest); lazy.SitePermissions.setForPrincipal( principal, @@ -1029,7 +1029,7 @@ function prompt(aActor, aBrowser, aRequest) { video.srcObject = stream; video.stream = stream; doc.getElementById("webRTC-preview").hidden = false; - video.onloadedmetadata = function (e) { + video.onloadedmetadata = function () { video.play(); }; }, diff --git a/browser/app/Makefile.in b/browser/app/Makefile.in index 7a09bc2494..08490ba27c 100644 --- a/browser/app/Makefile.in +++ b/browser/app/Makefile.in @@ -50,14 +50,17 @@ endif endif -# channel-prefs.js is handled separate from other prefs due to bug 756325 +ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT)) + +# channel-prefs.js has been removed on macOS. +# channel-prefs.js is handled separately from other prefs due to bug 756325. # DO NOT change the content of channel-prefs.js without taking the appropriate # steps. See bug 1431342. libs:: $(srcdir)/profile/channel-prefs.js $(NSINSTALL) -D $(DIST)/bin/defaults/pref $(call py_action,preprocessor channel-prefs.js,-Fsubstitution $(PREF_PPFLAGS) $(ACDEFINES) $^ -o $(DIST)/bin/defaults/pref/channel-prefs.js) -ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) +else MAC_APP_NAME = $(MOZ_APP_DISPLAYNAME) @@ -81,7 +84,7 @@ MAC_BUNDLE_VERSION = $(shell $(PYTHON3) $(srcdir)/macversion.py --version=$(MOZ_ .PHONY: repackage tools repackage:: $(DIST)/bin/$(MOZ_APP_NAME) $(objdir)/macbuild/Contents/MacOS-files.txt - rm -rf $(dist_dest) + rm -rf '$(dist_dest)' $(MKDIR) -p '$(dist_dest)/Contents/MacOS' $(MKDIR) -p '$(dist_dest)/$(LPROJ)' rsync -a --exclude '*.in' $(srcdir)/macbuild/Contents '$(dist_dest)' --exclude English.lproj @@ -99,8 +102,9 @@ tools repackage:: $(DIST)/bin/$(MOZ_APP_NAME) $(objdir)/macbuild/Contents/MacOS- cp -RL $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/document.icns '$(dist_dest)/Contents/Resources/document.icns' $(MKDIR) -p '$(dist_dest)/Contents/Library/LaunchServices' ifdef MOZ_UPDATER - mv -f '$(dist_dest)/Contents/MacOS/updater.app/Contents/MacOS/org.mozilla.updater' '$(dist_dest)/Contents/Library/LaunchServices' - ln -s ../../../../Library/LaunchServices/org.mozilla.updater '$(dist_dest)/Contents/MacOS/updater.app/Contents/MacOS/org.mozilla.updater' + cp -f '$(dist_dest)/Contents/MacOS/updater.app/Contents/MacOS/org.mozilla.updater' '$(dist_dest)/Contents/Library/LaunchServices' endif + $(MKDIR) -p '$(dist_dest)/Contents/Frameworks' + mv '$(dist_dest)/Contents/Resources/ChannelPrefs.framework' '$(dist_dest)/Contents/Frameworks' printf APPLMOZB > '$(dist_dest)/Contents/PkgInfo' endif diff --git a/browser/app/macbuild/Contents/Info.plist.in b/browser/app/macbuild/Contents/Info.plist.in index 48fc32199b..53a3d0f7ea 100644 --- a/browser/app/macbuild/Contents/Info.plist.in +++ b/browser/app/macbuild/Contents/Info.plist.in @@ -291,5 +291,8 @@ <key>NSMicrophoneUsageDescription</key> <string>Only sites you allow within @MAC_APP_NAME@ will be able to use the microphone.</string> + + <key>NSCameraReactionEffectGesturesEnabledDefault</key> + <false/> </dict> </plist> diff --git a/browser/app/nmhproxy/Cargo.toml b/browser/app/nmhproxy/Cargo.toml index 14746d51b6..d432773293 100644 --- a/browser/app/nmhproxy/Cargo.toml +++ b/browser/app/nmhproxy/Cargo.toml @@ -10,8 +10,10 @@ name = "nmhproxy" path = "src/main.rs" [dependencies] +dirs = "4" mozbuild = "0.1" mozilla-central-workspace-hack = { version = "0.1", features = ["nmhproxy"], optional = true } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1.0" +tempfile = "3" url = "2.4" diff --git a/browser/app/nmhproxy/src/commands.rs b/browser/app/nmhproxy/src/commands.rs index 29c86a0dd7..b26180e8f8 100644 --- a/browser/app/nmhproxy/src/commands.rs +++ b/browser/app/nmhproxy/src/commands.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use std::io::{self, Read, Write}; +use std::path::PathBuf; use std::process::Command; use url::Url; @@ -23,6 +24,7 @@ pub enum FirefoxCommand { LaunchFirefox { url: String }, LaunchFirefoxPrivate { url: String }, GetVersion {}, + GetInstallId {}, } #[derive(Serialize, Deserialize)] // { @@ -34,6 +36,14 @@ pub struct Response { pub result_code: u32, } +#[derive(Serialize, Deserialize)] +// { +// "installation_id": "123ABC456", +// } +pub struct InstallationId { + pub installation_id: String, +} + #[repr(u32)] pub enum ResultCode { Success = 0, @@ -152,6 +162,28 @@ pub fn process_command(command: &FirefoxCommand) -> std::io::Result<bool> { Ok(true) } FirefoxCommand::GetVersion {} => generate_response("1", ResultCode::Success.into()), + FirefoxCommand::GetInstallId {} => { + // config_dir() evaluates to ~/Library/Application Support on macOS + // and %RoamingAppData% on Windows. + let mut json_path = match dirs::config_dir() { + Some(path) => path, + None => { + return generate_response( + "Config dir could not be found", + ResultCode::Error.into(), + ) + } + }; + #[cfg(target_os = "windows")] + json_path.push("Mozilla\\Firefox"); + #[cfg(target_os = "macos")] + json_path.push("Firefox"); + + json_path.push("install_id"); + json_path.set_extension("json"); + let mut install_id = String::new(); + get_install_id(&mut json_path, &mut install_id) + } } } @@ -228,10 +260,54 @@ fn launch_firefox<C: CommandRunner>( command.to_string() } +fn get_install_id(json_path: &mut PathBuf, install_id: &mut String) -> std::io::Result<bool> { + if !json_path.exists() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Install ID file does not exist", + )); + } + let json_size = std::fs::metadata(&json_path) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))? + .len(); + // Set a 1 KB limit for the file size. + if json_size <= 0 || json_size > 1024 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Install ID file has invalid size", + )); + } + let mut file = + std::fs::File::open(json_path).or_else(|_| -> std::io::Result<std::fs::File> { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Failed to open file", + )); + })?; + let mut contents = String::new(); + match file.read_to_string(&mut contents) { + Ok(_) => match serde_json::from_str::<InstallationId>(&contents) { + Ok(id) => { + *install_id = id.installation_id.clone(); + generate_response(&id.installation_id, ResultCode::Success.into()) + } + Err(_) => { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Failed to read installation ID", + )) + } + }, + Err(_) => generate_response("Failed to read file", ResultCode::Error.into()), + }?; + Ok(true) +} + #[cfg(test)] mod tests { use super::*; use std::io::Cursor; + use tempfile::NamedTempFile; #[test] fn test_validate_url() { let valid_test_cases = vec![ @@ -347,4 +423,90 @@ mod tests { let correct_url_format = format!("-osint -private-window {}", url); assert!(command_line.contains(correct_url_format.as_str())); } + + #[test] + fn test_get_install_id_valid() -> std::io::Result<()> { + let mut tempfile = NamedTempFile::new().unwrap(); + let installation_id = InstallationId { + installation_id: "123ABC456".to_string(), + }; + let json_string = serde_json::to_string(&installation_id); + let _ = tempfile.write_all(json_string?.as_bytes()); + let mut install_id = String::new(); + let result = get_install_id(&mut tempfile.path().to_path_buf(), &mut install_id); + assert!(result.is_ok()); + assert_eq!(install_id, "123ABC456"); + Ok(()) + } + + #[test] + fn test_get_install_id_incorrect_var() -> std::io::Result<()> { + #[derive(Serialize, Deserialize)] + pub struct IncorrectJSON { + pub incorrect_var: String, + } + let mut tempfile = NamedTempFile::new().unwrap(); + let incorrect_json = IncorrectJSON { + incorrect_var: "incorrect_val".to_string(), + }; + let json_string = serde_json::to_string(&incorrect_json); + let _ = tempfile.write_all(json_string?.as_bytes()); + let mut install_id = String::new(); + let result = get_install_id(&mut tempfile.path().to_path_buf(), &mut install_id); + assert!(result.is_err()); + let error = result.err().unwrap(); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); + Ok(()) + } + + #[test] + fn test_get_install_id_partially_correct_vars() -> std::io::Result<()> { + #[derive(Serialize, Deserialize)] + pub struct IncorrectJSON { + pub installation_id: String, + pub incorrect_var: String, + } + let mut tempfile = NamedTempFile::new().unwrap(); + let incorrect_json = IncorrectJSON { + installation_id: "123ABC456".to_string(), + incorrect_var: "incorrect_val".to_string(), + }; + let json_string = serde_json::to_string(&incorrect_json); + let _ = tempfile.write_all(json_string?.as_bytes()); + let mut install_id = String::new(); + let result = get_install_id(&mut tempfile.path().to_path_buf(), &mut install_id); + // This still succeeds as the installation_id field is present + assert!(result.is_ok()); + Ok(()) + } + + #[test] + fn test_get_install_id_file_does_not_exist() -> std::io::Result<()> { + let tempfile = NamedTempFile::new().unwrap(); + let mut path = tempfile.path().to_path_buf(); + tempfile.close()?; + let mut install_id = String::new(); + let result = get_install_id(&mut path, &mut install_id); + assert!(result.is_err()); + let error = result.err().unwrap(); + assert_eq!(error.kind(), std::io::ErrorKind::NotFound); + Ok(()) + } + + #[test] + fn test_get_install_id_file_too_large() -> std::io::Result<()> { + let mut tempfile = NamedTempFile::new().unwrap(); + let installation_id = InstallationId { + // Create a ~10 KB file + installation_id: String::from_utf8(vec![b'X'; 10000]).unwrap(), + }; + let json_string = serde_json::to_string(&installation_id); + let _ = tempfile.write_all(json_string?.as_bytes()); + let mut install_id = String::new(); + let result = get_install_id(&mut tempfile.path().to_path_buf(), &mut install_id); + assert!(result.is_err()); + let error = result.err().unwrap(); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidData); + Ok(()) + } } diff --git a/browser/app/nmhproxy/src/main.rs b/browser/app/nmhproxy/src/main.rs index de9cd8c2a3..02351eb0f1 100644 --- a/browser/app/nmhproxy/src/main.rs +++ b/browser/app/nmhproxy/src/main.rs @@ -43,9 +43,12 @@ fn main() -> Result<(), Error> { "Failed to deserialize message JSON", )); })?; - commands::process_command(&native_messaging_json).or_else(|_| -> Result<bool, _> { - commands::generate_response("Failed to process command", ResultCode::Error.into()) - .expect("JSON error"); + commands::process_command(&native_messaging_json).or_else(|e| -> Result<bool, _> { + commands::generate_response( + format!("Failed to process command: {}", e).as_str(), + ResultCode::Error.into(), + ) + .expect("JSON error"); return Err(Error::new( ErrorKind::InvalidInput, "Failed to process command", diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 32cd57b0ed..8c4b0e28e5 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -356,6 +356,14 @@ pref("browser.overlink-delay", 80); pref("browser.theme.colorway-closet", true); +#ifdef XP_MACOSX +#ifdef NIGHTLY_BUILD +pref("browser.theme.macos.native-theme", true); +#else +pref("browser.theme.macos.native-theme", false); +#endif +#endif + // Whether expired built-in colorways themes that are active or retained // should be allowed to check for updates and be updated to an AMO hosted // theme with the same id (as part of preparing to remove from mozilla-central @@ -391,7 +399,8 @@ pref("browser.urlbar.speculativeConnect.enabled", true); // search for bookmarklets typing "javascript: " followed by the actual query. pref("browser.urlbar.filter.javascript", true); -// Enable a certain level of urlbar logging to the Browser Console. See Log.jsm. +// Enable a certain level of urlbar logging to the Browser Console. See +// ConsoleInstance.webidl. pref("browser.urlbar.loglevel", "Error"); // the maximum number of results to show in autocomplete when doing richResults @@ -434,7 +443,7 @@ pref("browser.search.param.search_rich_suggestions", "fen"); pref("browser.urlbar.weather.featureGate", false); // Enable clipboard suggestions feature, the pref should be removed once stable. -pref("browser.urlbar.clipboard.featureGate", true); +pref("browser.urlbar.clipboard.featureGate", false); // When false, the weather suggestion will not be fetched when a VPN is // detected. When true, it will be fetched anyway. @@ -718,13 +727,6 @@ pref("browser.download.clearHistoryOnDelete", 0); pref("browser.helperApps.showOpenOptionForPdfJS", true); pref("browser.helperApps.showOpenOptionForViewableInternally", true); -// Whether search-config-v2 is enabled. -#ifdef NIGHTLY_BUILD -pref("browser.search.newSearchConfig.enabled", true); -#else -pref("browser.search.newSearchConfig.enabled", false); -#endif - // search engines URL pref("browser.search.searchEnginesURL", "https://addons.mozilla.org/%LOCALE%/firefox/search-engines/"); @@ -747,7 +749,11 @@ pref("browser.search.separatePrivateDefault.ui.banner.max", 0); pref("browser.search.serpEventTelemetry.enabled", true); // Enables search SERP telemetry page categorization. +#ifdef NIGHTLY_BUILD +pref("browser.search.serpEventTelemetryCategorization.enabled", true); +#else pref("browser.search.serpEventTelemetryCategorization.enabled", false); +#endif // Search Bar removal from the toolbar for users who haven’t used it in 120 // days @@ -811,6 +817,10 @@ pref("browser.shopping.experience2023.sidebarClosedCount", 0); // When conditions are met, shows a prompt on the shopping sidebar asking users if they want to disable auto-open behavior pref("browser.shopping.experience2023.showKeepSidebarClosedMessage", true); +// Enable display of megalist option in browser sidebar +// Keep it hidden from about:config for now. +// pref("browser.megalist.enabled", false); + // Enables the display of the Mozilla VPN banner in private browsing windows pref("browser.privatebrowsing.vpnpromourl", "https://vpn.mozilla.org/?utm_source=firefox-browser&utm_medium=firefox-%CHANNEL%-browser&utm_campaign=private-browsing-vpn-link"); @@ -1094,11 +1104,7 @@ pref("privacy.history.custom", false); // 6 - Last 24 hours pref("privacy.sanitize.timeSpan", 1); -#if defined(NIGHTLY_BUILD) -pref("privacy.sanitize.useOldClearHistoryDialog", false); -#else pref("privacy.sanitize.useOldClearHistoryDialog", true); -#endif pref("privacy.sanitize.clearOnShutdown.hasMigratedToNewPrefs", false); // flag to track migration of clear history dialog prefs, where cpd stands for @@ -1277,9 +1283,10 @@ pref("browser.sessionstore.idleDelay", 180); // 3 minutes pref("browser.sessionstore.privacy_level", 0); // how many tabs can be reopened (per window) pref("browser.sessionstore.max_tabs_undo", 25); -// how many windows can be reopened (per session) - on non-OS X platforms this -// pref may be ignored when dealing with pop-up windows to ensure proper startup -pref("browser.sessionstore.max_windows_undo", 3); +// how many windows will be saved and can be reopened per session - on non-macOS platforms this +// pref may be ignored when dealing with pop-up windows to ensure the user actually gets +// at least one window with a menu bar. +pref("browser.sessionstore.max_windows_undo", 5); // number of crashes that can occur before the about:sessionrestore page is displayed // (this pref has no effect if more than 6 hours have passed since the last crash) pref("browser.sessionstore.max_resumed_crashes", 1); @@ -1422,7 +1429,11 @@ pref("browser.bookmarks.editDialog.maxRecentFolders", 7); // On windows these levels are: // See - security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp // SetSecurityLevelForContentProcess() for what the different settings mean. - pref("security.sandbox.content.level", 6); + #if defined(NIGHTLY_BUILD) + pref("security.sandbox.content.level", 7); + #else + pref("security.sandbox.content.level", 6); + #endif // Pref controlling if messages relevant to sandbox violations are logged. pref("security.sandbox.logging.enabled", false); @@ -1678,22 +1689,16 @@ pref("browser.topsites.contile.sov.enabled", true); pref("browser.partnerlink.attributionURL", "https://topsites.services.mozilla.com/cid/"); pref("browser.partnerlink.campaign.topsites", "amzn_2020_a1"); -// Whether to show tab level system prompts opened via nsIPrompt(Service) as -// SubDialogs in the TabDialogBox (true) or as TabModalPrompt in the -// TabModalPromptBox (false). -pref("prompts.tabChromePromptSubDialog", true); - -// Whether to show the dialogs opened at the content level, such as -// alert() or prompt(), using a SubDialogManager in the TabDialogBox. -pref("prompts.contentPromptSubDialog", true); - -// Whether to show window-modal dialogs opened for browser windows -// in a SubDialog inside their parent, instead of an OS level window. -pref("prompts.windowPromptSubDialog", true); - // Activates preloading of the new tab url. pref("browser.newtab.preload", true); +// Preference to enable wallpaper selection in the Customize Menu of new tab page +pref("browser.newtabpage.activity-stream.newtabWallpapers.enabled", false); + +// Current new tab page background image. +pref("browser.newtabpage.activity-stream.newtabWallpapers.wallpaper-light", ""); +pref("browser.newtabpage.activity-stream.newtabWallpapers.wallpaper-dark", ""); + pref("browser.newtabpage.activity-stream.newNewtabExperience.colors", "#0090ED,#FF4F5F,#2AC3A2,#FF7139,#A172FF,#FFA437,#FF2A8A"); // Activity Stream prefs that control to which page to redirect @@ -1709,7 +1714,6 @@ pref("browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", // ASRouter provider configuration pref("browser.newtabpage.activity-stream.asrouter.providers.cfr", "{\"id\":\"cfr\",\"enabled\":true,\"type\":\"remote-settings\",\"collection\":\"cfr\",\"updateCycleInMs\":3600000}"); -pref("browser.newtabpage.activity-stream.asrouter.providers.whats-new-panel", "{\"id\":\"whats-new-panel\",\"enabled\":false,\"type\":\"remote-settings\",\"collection\":\"whats-new-panel\",\"updateCycleInMs\":3600000}"); pref("browser.newtabpage.activity-stream.asrouter.providers.message-groups", "{\"id\":\"message-groups\",\"enabled\":true,\"type\":\"remote-settings\",\"collection\":\"message-groups\",\"updateCycleInMs\":3600000}"); pref("browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", "{\"id\":\"messaging-experiments\",\"enabled\":true,\"type\":\"remote-experiments\",\"updateCycleInMs\":3600000}"); @@ -1821,9 +1825,6 @@ pref("browser.aboutwelcome.screens", ""); // Used to enable window modal onboarding pref("browser.aboutwelcome.showModal", false); -// The pref that controls if the What's New panel is enabled. -pref("browser.messaging-system.whatsNewPanel.enabled", true); - // Experiment Manager // See Console.sys.mjs LOG_LEVELS for all possible values pref("messaging-system.log", "warn"); @@ -1855,10 +1856,9 @@ pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false); // Try to convert PDFs sent as octet-stream pref("pdfjs.handleOctetStream", true); -pref("sidebar.companion", false); - // Is the sidebar positioned ahead of the content browser pref("sidebar.position_start", true); +pref("sidebar.revamp", false); pref("security.protectionspopup.recordEventTelemetry", true); pref("security.app_menu.recordEventTelemetry", true); @@ -2510,9 +2510,6 @@ pref("identity.fxaccounts.toolbar.pxiToolbarEnabled.monitorEnabled", true); pref("identity.fxaccounts.toolbar.pxiToolbarEnabled.relayEnabled", true); pref("identity.fxaccounts.toolbar.pxiToolbarEnabled.vpnEnabled", true); -// Check bundled omni JARs for corruption. -pref("corroborator.enabled", true); - // Toolbox preferences pref("devtools.toolbox.footer.height", 250); pref("devtools.toolbox.sidebar.width", 500); @@ -2520,7 +2517,8 @@ pref("devtools.toolbox.host", "bottom"); pref("devtools.toolbox.previousHost", "right"); pref("devtools.toolbox.selectedTool", "inspector"); pref("devtools.toolbox.zoomValue", "1"); -pref("devtools.toolbox.splitconsoleEnabled", false); +pref("devtools.toolbox.splitconsole.enabled", true); +pref("devtools.toolbox.splitconsole.open", false); pref("devtools.toolbox.splitconsoleHeight", 100); pref("devtools.toolbox.tabsOrder", ""); // This is only used for local Web Extension debugging, @@ -2550,7 +2548,6 @@ pref("devtools.popups.debug", false); // Toolbox Button preferences pref("devtools.command-button-pick.enabled", true); pref("devtools.command-button-frames.enabled", true); -pref("devtools.command-button-splitconsole.enabled", true); pref("devtools.command-button-responsive.enabled", true); pref("devtools.command-button-screenshot.enabled", false); pref("devtools.command-button-rulers.enabled", false); @@ -3030,10 +3027,14 @@ pref("browser.mailto.dualPrompt", false); // default mailto handler. pref("browser.mailto.prompt.os", true); -pref("browser.backup.enabled", false); +// Pref to initialize the BackupService soon after startup. +pref("browser.backup.enabled", true); // Pref to enable the new profiles pref("browser.profiles.enabled", false); pref("startup.homepage_override_url_nimbus", ""); pref("startup.homepage_override_nimbus_maxVersion", ""); + +// Pref to enable the content relevancy feature. +pref("toolkit.contentRelevancy.enabled", false); diff --git a/browser/base/content/aboutDialog-appUpdater.js b/browser/base/content/aboutDialog-appUpdater.js index 21bf83bc42..5a8cc0561b 100644 --- a/browser/base/content/aboutDialog-appUpdater.js +++ b/browser/base/content/aboutDialog-appUpdater.js @@ -28,7 +28,7 @@ var UPDATING_MIN_DISPLAY_TIME_MS = 1500; var gAppUpdater; -function onUnload(aEvent) { +function onUnload(_aEvent) { if (gAppUpdater) { gAppUpdater.destroy(); gAppUpdater = null; diff --git a/browser/base/content/aboutDialog.xhtml b/browser/base/content/aboutDialog.xhtml index e0fcce367a..55de242415 100644 --- a/browser/base/content/aboutDialog.xhtml +++ b/browser/base/content/aboutDialog.xhtml @@ -138,7 +138,7 @@ <label is="text-link" useoriginprincipal="true" href="about:credits" data-l10n-name="community-creditsLink"/> </description> <description class="text-blurb" id="contributeDesc" data-l10n-id="helpus"> - <label is="text-link" href="https://donate.mozilla.org/?utm_source=firefox&utm_medium=referral&utm_campaign=firefox_about&utm_content=firefox_about" data-l10n-name="helpus-donateLink"/> + <label is="text-link" href="https://foundation.mozilla.org/?form=firefox-about" data-l10n-name="helpus-donateLink"/> <label is="text-link" href="https://www.mozilla.org/contribute/?utm_source=firefox-browser&utm_medium=firefox-desktop&utm_campaign=about-dialog" data-l10n-name="helpus-getInvolvedLink"/> </description> </vbox> diff --git a/browser/base/content/appmenu-viewcache.inc.xhtml b/browser/base/content/appmenu-viewcache.inc.xhtml index 04bba182fb..6d838bd9d6 100644 --- a/browser/base/content/appmenu-viewcache.inc.xhtml +++ b/browser/base/content/appmenu-viewcache.inc.xhtml @@ -3,16 +3,6 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. <html:template id="appMenu-viewCache"> - <panelview id="appMenu-mainView" class="PanelUI-subView"> - <vbox class="panel-subview-body"> - <toolbarbutton id="appMenu-whatsnew-button" - class="subviewbutton subviewbutton-iconic subviewbutton-nav" - hidden="true" - closemenu="none" - oncommand="PanelUI.showSubView('PanelUI-whatsNew', this)"/> - </vbox> - </panelview> - <!-- This is a placeholder app menu which should be replaced with the "real" Proton app menu before the Proton pref starts getting enabled. --> <panelview id="appMenu-protonMainView" class="PanelUI-subView" @@ -134,7 +124,7 @@ # in the middle of the fullscreen transition otherwise. oncommand=" this.closest('panel').hidePopup(); - setTimeout(() => BrowserFullScreen(), 0); + setTimeout(() => BrowserCommands.fullScreen(), 0); " tooltip="dynamic-shortcut-tooltip"> <observes element="View:FullScreen" attribute="checked"/> @@ -759,29 +749,6 @@ </vbox> </panelview> - <panelview id="PanelUI-whatsNew" class="PanelUI-subView" mainview-with-header="true"> - <hbox id="PanelUI-whatsNew-title" class="panel-header"> - <html:h1> - <html:span data-l10n-id="whatsnew-panel-header"></html:span> - </html:h1> - </hbox> - <toolbarseparator/> - <vbox class="panel-subview-body"> - <toolbaritem id="PanelUI-whatsNew-content" - orient="vertical" - smoothscroll="false"> - <html:div id="PanelUI-whatsNew-message-container" role="document"> - <!-- What's New messages will be rendered here --> - </html:div> - </toolbaritem> - </vbox> - <toolbarseparator/> - <checkbox id="panelMenu-toggleWhatsNew" - class="panelMenu-toggleWhatsNew-checkbox" - onclick="ToolbarPanelHub.toggleWhatsNewPref(event)" - data-l10n-id="whatsnew-panel-footer-checkbox"/> - </panelview> - <panelview id="reset-pbm-panel" class="PanelUI-subView" role="document"> <vbox id="reset-pbm-panel-container" role="alertdialog" aria-labelledby="reset-pbm-panel-header"> <hbox id="reset-pbm-panel-header"> diff --git a/browser/base/content/browser-a11yUtils.js b/browser/base/content/browser-a11yUtils.js index 9bedb9238c..935ddc6a55 100644 --- a/browser/base/content/browser-a11yUtils.js +++ b/browser/base/content/browser-a11yUtils.js @@ -21,6 +21,7 @@ var A11yUtils = { * can thus hinder rather than help users if used incorrectly. * Please only use this after consultation with the Mozilla accessibility * team. + * @param {object} [options] * @param {string} [options.id] The Fluent id of the message to announce. The * ftl file must already be included in browser.xhtml. This must be * specified unless a raw message is specified instead. @@ -28,13 +29,8 @@ var A11yUtils = { * @param {string} [options.raw] The raw, already localized message to * announce. You should generally prefer a Fluent id instead, but in * rare cases, this might not be feasible. - * @param {Element} [options.source] The element with which the announcement - * is associated. This should generally be something the user can - * interact with to respond to the announcement. For example, for an - * announcement indicating that Reader View is available, this should - * be the Reader View button on the toolbar. */ - async announce({ id = null, args = {}, raw = null, source = document } = {}) { + async announce({ id = null, args = {}, raw = null } = {}) { if ((!id && !raw) || (id && raw)) { throw new Error("One of raw or id must be specified."); } diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js index 6f50745e8d..1e97428e24 100644 --- a/browser/base/content/browser-addons.js +++ b/browser/base/content/browser-addons.js @@ -525,7 +525,7 @@ var gXPInstallObserver = { Services.console.logMessage(consoleMsg); }, - async observe(aSubject, aTopic, aData) { + async observe(aSubject, aTopic) { var installInfo = aSubject.wrappedJSObject; var browser = installInfo.browser; @@ -914,9 +914,9 @@ var gXPInstallObserver = { let height = undefined; if (PopupNotifications.isPanelOpen) { - let rect = document - .getElementById("addon-progress-notification") - .getBoundingClientRect(); + let rect = window.windowUtils.getBoundsWithoutFlushing( + document.getElementById("addon-progress-notification") + ); height = rect.height; } @@ -1009,7 +1009,7 @@ var gExtensionsNotifications = { let items = 0; if (lazy.AMBrowserExtensionsImport.canCompleteOrCancelInstalls) { - this._createAddonButton("webext-imported-addons", null, evt => { + this._createAddonButton("webext-imported-addons", null, () => { lazy.AMBrowserExtensionsImport.completeInstalls(); }); items++; @@ -1022,7 +1022,7 @@ var gExtensionsNotifications = { this._createAddonButton( "webext-perms-update-menu-item", update.addon, - evt => { + () => { ExtensionsUI.showUpdate(gBrowser, update); } ); @@ -1032,7 +1032,7 @@ var gExtensionsNotifications = { if (++items > 4) { break; } - this._createAddonButton("webext-perms-sideload-menu-item", addon, evt => { + this._createAddonButton("webext-perms-sideload-menu-item", addon, () => { // We need to hide the main menu manually because the toolbarbutton is // removed immediately while processing this event, and PanelUI is // unable to identify which panel should be closed automatically. @@ -1046,16 +1046,11 @@ var gExtensionsNotifications = { var BrowserAddonUI = { async promptRemoveExtension(addon) { let { name } = addon; - let [title, btnTitle, message] = await lazy.l10n.formatValues([ + let [title, btnTitle] = await lazy.l10n.formatValues([ { id: "addon-removal-title", args: { name } }, { id: "addon-removal-button" }, - { id: "addon-removal-message", args: { name } }, ]); - if (Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) { - message = null; - } - let { BUTTON_TITLE_IS_STRING: titleString, BUTTON_TITLE_CANCEL: titleCancel, @@ -1082,7 +1077,7 @@ var BrowserAddonUI = { let result = confirmEx( window, title, - message, + null, btnFlags, btnTitle, /* button1 */ null, @@ -1117,7 +1112,7 @@ var BrowserAddonUI = { win.openAbuseReport({ addonId, reportEntryPoint }); }, - async removeAddon(addonId, eventObject) { + async removeAddon(addonId) { let addon = addonId && (await AddonManager.getAddonByID(addonId)); if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) { return; @@ -1136,7 +1131,7 @@ var BrowserAddonUI = { } }, - async manageAddon(addonId, eventObject) { + async manageAddon(addonId) { let addon = addonId && (await AddonManager.getAddonByID(addonId)); if (!addon) { return; @@ -1798,7 +1793,7 @@ var gUnifiedExtensions = { } }, - onWidgetAdded(aWidgetId, aArea, aPosition) { + onWidgetAdded(aWidgetId, aArea) { // When we pin a widget to the toolbar from a narrow window, the widget // will be overflowed directly. In this case, we do not want to change the // class name since it is going to be changed by `onWidgetOverflow()` @@ -1813,7 +1808,7 @@ var gUnifiedExtensions = { this._updateWidgetClassName(aWidgetId, inPanel); }, - onWidgetOverflow(aNode, aContainer) { + onWidgetOverflow(aNode) { // We register a CUI listener for each window so we make sure that we // handle the event for the right window here. if (window !== aNode.ownerGlobal) { @@ -1823,7 +1818,7 @@ var gUnifiedExtensions = { this._updateWidgetClassName(aNode.getAttribute("widget-id"), true); }, - onWidgetUnderflow(aNode, aContainer) { + onWidgetUnderflow(aNode) { // We register a CUI listener for each window so we make sure that we // handle the event for the right window here. if (window !== aNode.ownerGlobal) { diff --git a/browser/base/content/browser-allTabsMenu.js b/browser/base/content/browser-allTabsMenu.js index f11d4da71d..a9caeffca0 100644 --- a/browser/base/content/browser-allTabsMenu.js +++ b/browser/base/content/browser-allTabsMenu.js @@ -58,7 +58,7 @@ var gTabsPanel = { dropIndicator: this.dropIndicator, }); - this.allTabsView.addEventListener("ViewShowing", e => { + this.allTabsView.addEventListener("ViewShowing", () => { PanelUI._ensureShortcutsShown(this.allTabsView); let containersEnabled = @@ -74,7 +74,7 @@ var gTabsPanel = { !hasHiddenTabs; }); - this.allTabsView.addEventListener("ViewShown", e => + this.allTabsView.addEventListener("ViewShown", () => this.allTabsView .querySelector(".all-tabs-item[selected]") ?.scrollIntoView({ block: "center" }) @@ -170,7 +170,7 @@ var gTabsPanel = { } this.allTabsView.addEventListener( "ViewShown", - e => { + () => { PanelUI.showSubView( this.kElements.hiddenTabsView, this.hiddenTabsButton diff --git a/browser/base/content/browser-box.inc.xhtml b/browser/base/content/browser-box.inc.xhtml index d445abe7e7..41258d81bb 100644 --- a/browser/base/content/browser-box.inc.xhtml +++ b/browser/base/content/browser-box.inc.xhtml @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. <hbox flex="1" id="browser"> + <html:sidebar-launcher id="sidebar-launcher" flex="1" hidden="true"></html:sidebar-launcher> <vbox id="sidebar-box" hidden="true" class="chromeclass-extrachrome"> <box id="sidebar-header" align="center"> <toolbarbutton id="sidebar-switcher-target" class="tabbable" aria-expanded="false"> diff --git a/browser/base/content/browser-captivePortal.js b/browser/base/content/browser-captivePortal.js index 247f8c397f..1fd5497273 100644 --- a/browser/base/content/browser-captivePortal.js +++ b/browser/base/content/browser-captivePortal.js @@ -95,7 +95,7 @@ var CaptivePortalWatcher = { } }, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "captive-portal-login": this._captivePortalDetected(); diff --git a/browser/base/content/browser-commands.js b/browser/base/content/browser-commands.js new file mode 100644 index 0000000000..40eb4d5baa --- /dev/null +++ b/browser/base/content/browser-commands.js @@ -0,0 +1,576 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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/. */ + +/* eslint-env mozilla/browser-window */ + +"use strict"; + +var kSkipCacheFlags = + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + +var BrowserCommands = { + back(aEvent) { + const where = BrowserUtils.whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goBack(); + } catch (ex) {} + } else { + duplicateTabIn(gBrowser.selectedTab, where, -1); + } + }, + + forward(aEvent) { + const where = BrowserUtils.whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goForward(); + } catch (ex) {} + } else { + duplicateTabIn(gBrowser.selectedTab, where, 1); + } + }, + + handleBackspace() { + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + this.back(); + break; + case 1: + goDoCommand("cmd_scrollPageUp"); + break; + } + }, + + handleShiftBackspace() { + switch (Services.prefs.getIntPref("browser.backspace_action")) { + case 0: + this.forward(); + break; + case 1: + goDoCommand("cmd_scrollPageDown"); + break; + } + }, + + gotoHistoryIndex(aEvent) { + aEvent = BrowserUtils.getRootEvent(aEvent); + + const index = aEvent.target.getAttribute("index"); + if (!index) { + return false; + } + + const where = BrowserUtils.whereToOpenLink(aEvent); + + if (where == "current") { + // Normal click. Go there in the current tab and update session history. + + try { + gBrowser.gotoIndex(index); + } catch (ex) { + return false; + } + return true; + } + // Modified click. Go there in a new tab/window. + + const historyindex = aEvent.target.getAttribute("historyindex"); + duplicateTabIn(gBrowser.selectedTab, where, Number(historyindex)); + return true; + }, + + reloadOrDuplicate(aEvent) { + aEvent = BrowserUtils.getRootEvent(aEvent); + const accelKeyPressed = + AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey; + const backgroundTabModifier = aEvent.button == 1 || accelKeyPressed; + + if (aEvent.shiftKey && !backgroundTabModifier) { + this.reloadSkipCache(); + return; + } + + const where = BrowserUtils.whereToOpenLink(aEvent, false, true); + if (where == "current") { + this.reload(); + } else { + duplicateTabIn(gBrowser.selectedTab, where); + } + }, + + reload() { + if (gBrowser.currentURI.schemeIs("view-source")) { + // Bug 1167797: For view source, we always skip the cache + this.reloadSkipCache(); + return; + } + this.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_NONE); + }, + + reloadSkipCache() { + // Bypass proxy and cache. + this.reloadWithFlags(kSkipCacheFlags); + }, + + reloadWithFlags(reloadFlags) { + const unchangedRemoteness = []; + + for (const tab of gBrowser.selectedTabs) { + const browser = tab.linkedBrowser; + const url = browser.currentURI; + const urlSpec = url.spec; + // We need to cache the content principal here because the browser will be + // reconstructed when the remoteness changes and the content prinicpal will + // be cleared after reconstruction. + const principal = tab.linkedBrowser.contentPrincipal; + if (gBrowser.updateBrowserRemotenessByURL(browser, urlSpec)) { + // If the remoteness has changed, the new browser doesn't have any + // information of what was loaded before, so we need to load the previous + // URL again. + if (tab.linkedPanel) { + loadBrowserURI(browser, url, principal); + } else { + // Shift to fully loaded browser and make + // sure load handler is instantiated. + tab.addEventListener( + "SSTabRestoring", + () => loadBrowserURI(browser, url, principal), + { once: true } + ); + gBrowser._insertBrowser(tab); + } + } else { + unchangedRemoteness.push(tab); + } + } + + if (!unchangedRemoteness.length) { + return; + } + + // Reset temporary permissions on the remaining tabs to reload. + // This is done here because we only want to reset + // permissions on user reload. + for (const tab of unchangedRemoteness) { + SitePermissions.clearTemporaryBlockPermissions(tab.linkedBrowser); + // Also reset DOS mitigations for the basic auth prompt on reload. + delete tab.linkedBrowser.authPromptAbuseCounter; + } + gIdentityHandler.hidePopup(); + gPermissionPanel.hidePopup(); + + const handlingUserInput = document.hasValidTransientUserGestureActivation; + + for (const tab of unchangedRemoteness) { + if (tab.linkedPanel) { + sendReloadMessage(tab); + } else { + // Shift to fully loaded browser and make + // sure load handler is instantiated. + tab.addEventListener("SSTabRestoring", () => sendReloadMessage(tab), { + once: true, + }); + gBrowser._insertBrowser(tab); + } + } + + function loadBrowserURI(browser, url, principal) { + browser.loadURI(url, { + flags: reloadFlags, + triggeringPrincipal: principal, + }); + } + + function sendReloadMessage(tab) { + tab.linkedBrowser.sendMessageToActor( + "Browser:Reload", + { flags: reloadFlags, handlingUserInput }, + "BrowserTab" + ); + } + }, + + stop() { + gBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL); + }, + + home(aEvent) { + if (aEvent?.button == 2) { + // right-click: do nothing + return; + } + + const homePage = HomePage.get(window); + let where = BrowserUtils.whereToOpenLink(aEvent, false, true); + + // Don't load the home page in pinned or hidden tabs (e.g. Firefox View). + if ( + where == "current" && + (gBrowser?.selectedTab.pinned || gBrowser?.selectedTab.hidden) + ) { + where = "tab"; + } + + // openTrustedLinkIn in utilityOverlay.js doesn't handle loading multiple pages + let notifyObservers; + switch (where) { + case "current": + // If we're going to load an initial page in the current tab as the + // home page, we set initialPageLoadedFromURLBar so that the URL + // bar is cleared properly (even during a remoteness flip). + if (isInitialPage(homePage)) { + gBrowser.selectedBrowser.initialPageLoadedFromUserAction = homePage; + } + loadOneOrMoreURIs( + homePage, + Services.scriptSecurityManager.getSystemPrincipal(), + null + ); + if (isBlankPageURL(homePage)) { + gURLBar.select(); + } else { + gBrowser.selectedBrowser.focus(); + } + notifyObservers = true; + aEvent?.preventDefault(); + break; + case "tabshifted": + case "tab": { + const urls = homePage.split("|"); + const loadInBackground = Services.prefs.getBoolPref( + "browser.tabs.loadBookmarksInBackground", + false + ); + // The homepage observer event should only be triggered when the homepage opens + // in the foreground. This is mostly to support the homepage changed by extension + // doorhanger which doesn't currently support background pages. This may change in + // bug 1438396. + notifyObservers = !loadInBackground; + gBrowser.loadTabs(urls, { + inBackground: loadInBackground, + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + csp: null, + }); + if (!loadInBackground) { + if (isBlankPageURL(homePage)) { + gURLBar.select(); + } else { + gBrowser.selectedBrowser.focus(); + } + } + aEvent?.preventDefault(); + break; + } + case "window": + // OpenBrowserWindow will trigger the observer event, so no need to do so here. + notifyObservers = false; + OpenBrowserWindow(); + aEvent?.preventDefault(); + break; + } + + if (notifyObservers) { + // A notification for when a user has triggered their homepage. This is used + // to display a doorhanger explaining that an extension has modified the + // homepage, if necessary. Observers are only notified if the homepage + // becomes the active page. + Services.obs.notifyObservers(null, "browser-open-homepage-start"); + } + }, + + openTab({ event, url } = {}) { + let werePassedURL = !!url; + url ??= BROWSER_NEW_TAB_URL; + let searchClipboard = + gMiddleClickNewTabUsesPasteboard && event?.button == 1; + + let relatedToCurrent = false; + let where = "tab"; + + if (event) { + where = whereToOpenLink(event, false, true); + + switch (where) { + case "tab": + case "tabshifted": + // When accel-click or middle-click are used, open the new tab as + // related to the current tab. + relatedToCurrent = true; + break; + case "current": + where = "tab"; + break; + } + } + + // A notification intended to be useful for modular peformance tracking + // starting as close as is reasonably possible to the time when the user + // expressed the intent to open a new tab. Since there are a lot of + // entry points, this won't catch every single tab created, but most + // initiated by the user should go through here. + // + // Note 1: This notification gets notified with a promise that resolves + // with the linked browser when the tab gets created + // Note 2: This is also used to notify a user that an extension has changed + // the New Tab page. + Services.obs.notifyObservers( + { + wrappedJSObject: new Promise(resolve => { + let options = { + relatedToCurrent, + resolveOnNewTabCreated: resolve, + }; + if (!werePassedURL && searchClipboard) { + let clipboard = readFromClipboard(); + clipboard = + UrlbarUtils.stripUnsafeProtocolOnPaste(clipboard).trim(); + if (clipboard) { + url = clipboard; + options.allowThirdPartyFixup = true; + } + } + openTrustedLinkIn(url, where, options); + }), + }, + "browser-open-newtab-start" + ); + }, + + openFileWindow() { + // Get filepicker component. + try { + const nsIFilePicker = Ci.nsIFilePicker; + const fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + const fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK) { + try { + if (fp.file) { + gLastOpenDirectory.path = fp.file.parent.QueryInterface( + Ci.nsIFile + ); + } + } catch (ex) {} + openTrustedLinkIn(fp.fileURL.spec, "current"); + } + }; + + fp.init( + window.browsingContext, + gNavigatorBundle.getString("openFile"), + nsIFilePicker.modeOpen + ); + fp.appendFilters( + nsIFilePicker.filterAll | + nsIFilePicker.filterText | + nsIFilePicker.filterImages | + nsIFilePicker.filterXML | + nsIFilePicker.filterHTML | + nsIFilePicker.filterPDF + ); + fp.displayDirectory = gLastOpenDirectory.path; + fp.open(fpCallback); + } catch (ex) {} + }, + + closeTabOrWindow(event) { + // If we're not a browser window, just close the window. + if (window.location.href != AppConstants.BROWSER_CHROME_URL) { + closeWindow(true); + return; + } + + // In a multi-select context, close all selected tabs + if (gBrowser.multiSelectedTabsCount) { + gBrowser.removeMultiSelectedTabs(); + return; + } + + // Keyboard shortcuts that would close a tab that is pinned select the first + // unpinned tab instead. + if ( + event && + (event.ctrlKey || event.metaKey || event.altKey) && + gBrowser.selectedTab.pinned + ) { + if (gBrowser.visibleTabs.length > gBrowser._numPinnedTabs) { + gBrowser.tabContainer.selectedIndex = gBrowser._numPinnedTabs; + } + return; + } + + // If the current tab is the last one, this will close the window. + gBrowser.removeCurrentTab({ animate: true }); + }, + + tryToCloseWindow(event) { + if (WindowIsClosing(event)) { + window.close(); + } // WindowIsClosing does all the necessary checks + }, + + /** + * Open the View Source dialog. + * + * @param args + * An object with the following properties: + * + * URL (required): + * A string URL for the page we'd like to view the source of. + * browser (optional): + * The browser containing the document that we would like to view the + * source of. This is required if outerWindowID is passed. + * outerWindowID (optional): + * The outerWindowID of the content window containing the document that + * we want to view the source of. You only need to provide this if you + * want to attempt to retrieve the document source from the network + * cache. + * lineNumber (optional): + * The line number to focus on once the source is loaded. + */ + async viewSourceOfDocument(args) { + // Check if external view source is enabled. If so, try it. If it fails, + // fallback to internal view source. + if (Services.prefs.getBoolPref("view_source.editor.external")) { + try { + await top.gViewSourceUtils.openInExternalEditor(args); + return; + } catch (data) {} + } + + let tabBrowser = gBrowser; + let preferredRemoteType; + let initialBrowsingContextGroupId; + if (args.browser) { + preferredRemoteType = args.browser.remoteType; + initialBrowsingContextGroupId = args.browser.browsingContext.group.id; + } else { + if (!tabBrowser) { + throw new Error( + "viewSourceOfDocument should be passed the " + + "subject browser if called from a window without " + + "gBrowser defined." + ); + } + // Some internal URLs (such as specific chrome: and about: URLs that are + // not yet remote ready) cannot be loaded in a remote browser. View + // source in tab expects the new view source browser's remoteness to match + // that of the original URL, so disable remoteness if necessary for this + // URL. + const oa = E10SUtils.predictOriginAttributes({ window }); + preferredRemoteType = E10SUtils.getRemoteTypeForURI( + args.URL, + gMultiProcessBrowser, + gFissionBrowser, + E10SUtils.DEFAULT_REMOTE_TYPE, + null, + oa + ); + } + + // In the case of popups, we need to find a non-popup browser window. + if (!tabBrowser || !window.toolbar.visible) { + // This returns only non-popup browser windows by default. + const browserWindow = BrowserWindowTracker.getTopWindow(); + tabBrowser = browserWindow.gBrowser; + } + + const inNewWindow = !Services.prefs.getBoolPref("view_source.tab"); + + // `viewSourceInBrowser` will load the source content from the page + // descriptor for the tab (when possible) or fallback to the network if + // that fails. Either way, the view source module will manage the tab's + // location, so use "about:blank" here to avoid unnecessary redundant + // requests. + const tab = tabBrowser.addTab("about:blank", { + relatedToCurrent: true, + inBackground: inNewWindow, + skipAnimation: inNewWindow, + preferredRemoteType, + initialBrowsingContextGroupId, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + skipLoad: true, + }); + args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab); + top.gViewSourceUtils.viewSourceInBrowser(args); + + if (inNewWindow) { + tabBrowser.hideTab(tab); + tabBrowser.replaceTabWithWindow(tab); + } + }, + + /** + * Opens the View Source dialog for the source loaded in the root + * top-level document of the browser. This is really just a + * convenience wrapper around viewSourceOfDocument. + * + * @param browser + * The browser that we want to load the source of. + */ + viewSource(browser) { + this.viewSourceOfDocument({ + browser, + outerWindowID: browser.outerWindowID, + URL: browser.currentURI.spec, + }); + }, + + /** + * @param documentURL URL of the document to view, or null for this window's document + * @param initialTab name of the initial tab to display, or null for the first tab + * @param imageElement image to load in the Media Tab of the Page Info window; can be null/omitted + * @param browsingContext the browsingContext of the frame that we want to view information about; can be null/omitted + * @param browser the browser containing the document we're interested in inspecting; can be null/omitted + */ + pageInfo(documentURL, initialTab, imageElement, browsingContext, browser) { + const args = { initialTab, imageElement, browsingContext, browser }; + + documentURL = + documentURL || window.gBrowser.selectedBrowser.currentURI.spec; + + const isPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + + // Check for windows matching the url + for (const currentWindow of Services.wm.getEnumerator( + "Browser:page-info" + )) { + if (currentWindow.closed) { + continue; + } + if ( + currentWindow.document.documentElement.getAttribute("relatedUrl") == + documentURL && + PrivateBrowsingUtils.isWindowPrivate(currentWindow) == isPrivate + ) { + currentWindow.focus(); + currentWindow.resetPageInfo(args); + return currentWindow; + } + } + + // We didn't find a matching window, so open a new one. + let options = "chrome,toolbar,dialog=no,resizable"; + + // Ensure the window groups correctly in the Windows taskbar + if (isPrivate) { + options += ",private"; + } + return openDialog( + "chrome://browser/content/pageinfo/pageInfo.xhtml", + "", + options, + args + ); + }, + + fullScreen() { + window.fullScreen = !window.fullScreen || BrowserHandler.kiosk; + }, +}; diff --git a/browser/base/content/browser-context.inc b/browser/base/content/browser-context.inc index ff4015e3d4..3ce284a52f 100644 --- a/browser/base/content/browser-context.inc +++ b/browser/base/content/browser-context.inc @@ -439,7 +439,7 @@ oncommand="gContextMenu.viewPartialSource();"/> <menuitem id="context-viewsource" data-l10n-id="main-context-menu-view-page-source" - oncommand="BrowserViewSource(gContextMenu.browser);"/> + oncommand="BrowserCommands.viewSource(gContextMenu.browser);"/> <menuitem id="context-inspect-a11y" hidden="true" data-l10n-id="main-context-menu-inspect-a11y-properties" diff --git a/browser/base/content/browser-ctrlTab.js b/browser/base/content/browser-ctrlTab.js index d4d79a6886..e5d16e605b 100644 --- a/browser/base/content/browser-ctrlTab.js +++ b/browser/base/content/browser-ctrlTab.js @@ -284,7 +284,7 @@ var ctrlTab = { this.uninit(); } }, - observe(aSubject, aTopic, aPrefName) { + observe() { this.readPref(); }, @@ -654,7 +654,7 @@ var ctrlTab = { // tab attribute modified (i.e. label, busy, image) // update preview only if tab attribute modified in the list if ( - event.detail.changed.some((elem, ind, arr) => + event.detail.changed.some(elem => ["label", "busy", "image"].includes(elem) ) ) { diff --git a/browser/base/content/browser-data-submission-info-bar.js b/browser/base/content/browser-data-submission-info-bar.js index 26d9affb29..104414d582 100644 --- a/browser/base/content/browser-data-submission-info-bar.js +++ b/browser/base/content/browser-data-submission-info-bar.js @@ -92,7 +92,7 @@ var gDataNotificationInfoBar = { } }, - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "datareporting:notify-data-policy:request": let request = subject.wrappedJSObject.object; diff --git a/browser/base/content/browser-fullScreenAndPointerLock.js b/browser/base/content/browser-fullScreenAndPointerLock.js index aae596a0f7..c8794c760c 100644 --- a/browser/base/content/browser-fullScreenAndPointerLock.js +++ b/browser/base/content/browser-fullScreenAndPointerLock.js @@ -365,26 +365,19 @@ var FullScreen = { passive: true, }); - if (enterFS) { - gNavToolbox.setAttribute("inFullscreen", true); - document.documentElement.setAttribute("inFullscreen", true); - let alwaysUsesNativeFullscreen = + document.documentElement.toggleAttribute("inFullscreen", enterFS); + document.documentElement.toggleAttribute( + "macOSNativeFullscreen", + enterFS && AppConstants.platform == "macosx" && - Services.prefs.getBoolPref("full-screen-api.macos-native-full-screen"); - if ( - (alwaysUsesNativeFullscreen || !document.fullscreenElement) && - AppConstants.platform == "macosx" - ) { - document.documentElement.setAttribute("macOSNativeFullscreen", true); - } - } else { - gNavToolbox.removeAttribute("inFullscreen"); - document.documentElement.removeAttribute("inFullscreen"); - document.documentElement.removeAttribute("macOSNativeFullscreen"); - } + (Services.prefs.getBoolPref( + "full-screen-api.macos-native-full-screen" + ) || + !document.fullscreenElement) + ); if (!document.fullscreenElement) { - this._updateToolbars(enterFS); + ToolbarIconColor.inferFromText("fullscreen", enterFS); } if (enterFS) { @@ -948,22 +941,6 @@ var FullScreen = { MousePosTracker.removeListener(this); }, - - _updateToolbars(aEnterFS) { - for (let el of document.querySelectorAll( - "toolbar[fullscreentoolbar=true]" - )) { - // Set the inFullscreen attribute to allow specific styling - // in fullscreen mode - if (aEnterFS) { - el.setAttribute("inFullscreen", true); - } else { - el.removeAttribute("inFullscreen"); - } - } - - ToolbarIconColor.inferFromText("fullscreen", aEnterFS); - }, }; ChromeUtils.defineLazyGetter(FullScreen, "_permissionNotificationIDs", () => { diff --git a/browser/base/content/browser-gestureSupport.js b/browser/base/content/browser-gestureSupport.js index 0ff42f5160..6bec059789 100644 --- a/browser/base/content/browser-gestureSupport.js +++ b/browser/base/content/browser-gestureSupport.js @@ -269,7 +269,7 @@ var gGestureSupport = { gHistorySwipeAnimation.updateAnimation(aEvent.delta); }; - this._doEnd = function GS__doEnd(aEvent) { + this._doEnd = function GS__doEnd() { gHistorySwipeAnimation.swipeEndEventReceived(); this._doUpdate = function () {}; @@ -393,7 +393,7 @@ var gGestureSupport = { * @param aEvent * The continual motion update event to handle */ - _doUpdate(aEvent) {}, + _doUpdate() {}, /** * Handle gesture end events. This function will be set by _setupSwipe. @@ -401,7 +401,7 @@ var gGestureSupport = { * @param aEvent * The gesture end event to handle */ - _doEnd(aEvent) {}, + _doEnd() {}, /** * Convert the swipe gesture into a browser action based on the direction. @@ -874,7 +874,7 @@ var gHistorySwipeAnimation = { } }, - _completeFadeOut: function HSA__completeFadeOut(aEvent) { + _completeFadeOut: function HSA__completeFadeOut() { if (!this._isStoppingAnimation) { // The animation was restarted in the middle of our stopping fade out // tranistion, so don't do anything. @@ -943,7 +943,7 @@ var gHistorySwipeAnimation = { return element; }, - observe(subj, topic, data) { + observe(subj, topic) { switch (topic) { case "nsPref:changed": this._initPrefValues(); diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc index 66dd662137..0c8b4a1cf6 100644 --- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -140,7 +140,7 @@ </menupopup> </menu> <menu id="viewSidebarMenuMenu" data-l10n-id="menu-view-sidebar"> - <menupopup id="viewSidebarMenu"> + <menupopup id="viewSidebarMenu" onpopupshowing="SidebarUI.setMegalistMenubarVisibility(event);"> <menuitem id="menu_bookmarksSidebar" type="checkbox" key="viewBookmarksSidebarKb" @@ -153,6 +153,9 @@ type="checkbox" class="sync-ui-item" oncommand="SidebarUI.toggle('viewTabsSidebar');" data-l10n-id="menu-view-synced-tabs-sidebar"/> + <menuitem id="menu_megalistSidebar" + type="checkbox" + oncommand="SidebarUI.toggle('viewMegalistSidebar');" data-l10n-id="menu-view-megalist-sidebar"/> </menupopup> </menu> <menuseparator/> diff --git a/browser/base/content/browser-pageActions.js b/browser/base/content/browser-pageActions.js index 1cc895434d..1fd1062948 100644 --- a/browser/base/content/browser-pageActions.js +++ b/browser/base/content/browser-pageActions.js @@ -1008,7 +1008,7 @@ BrowserPageActions.bookmark = { } }, - onCommand(event, buttonNode) { + onCommand(event) { PanelMultiView.hidePopup(BrowserPageActions.panelNode); BookmarkingUI.onStarCommand(event); }, diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js index 58e61f7bf7..c940fade5f 100644 --- a/browser/base/content/browser-places.js +++ b/browser/base/content/browser-places.js @@ -17,7 +17,7 @@ XPCOMUtils.defineLazyPreferenceGetter( "SHOW_OTHER_BOOKMARKS", "browser.toolbars.bookmarks.showOtherBookmarks", true, - (aPref, aPrevVal, aNewVal) => { + () => { BookmarkingUI.maybeShowOtherBookmarksFolder().then(() => { document .getElementById("PlacesToolbar") @@ -253,7 +253,7 @@ var StarUI = { } target.addEventListener( "popupshown", - function (event) { + function () { fn(); }, { capture: true, once: true } @@ -1174,7 +1174,7 @@ var PlacesToolbarHelper = { return null; }, - onWidgetUnderflow(aNode, aContainer) { + onWidgetUnderflow(aNode) { // The view gets broken by being removed and reinserted by the overflowable // toolbar, so we have to force an uninit and reinit. let win = aNode.ownerGlobal; @@ -1183,7 +1183,7 @@ var PlacesToolbarHelper = { } }, - onWidgetAdded(aWidgetId, aArea, aPosition) { + onWidgetAdded(aWidgetId) { if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) { // It's possible (with the "Add to Menu", "Add to Toolbar" context // options) that the Places Toolbar Items have been moved without @@ -1659,13 +1659,13 @@ var BookmarkingUI = { } }, - onWidgetReset: function BUI_widgetReset(aNode, aContainer) { + onWidgetReset: function BUI_widgetReset(aNode) { if (aNode == this.button) { this._onWidgetWasMoved(); } }, - onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode, aContainer) { + onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode) { if (aNode == this.button) { this._onWidgetWasMoved(); } @@ -2155,7 +2155,7 @@ var BookmarkingUI = { }); }, - onWidgetUnderflow(aNode, aContainer) { + onWidgetUnderflow(aNode) { let win = aNode.ownerGlobal; if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window) { return; diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc index 090e94b684..e247f0641e 100644 --- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -16,12 +16,12 @@ <commandset id="mainCommandSet"> <command id="cmd_newNavigator" oncommand="OpenBrowserWindow()"/> - <command id="cmd_handleBackspace" oncommand="BrowserHandleBackspace();" /> - <command id="cmd_handleShiftBackspace" oncommand="BrowserHandleShiftBackspace();" /> + <command id="cmd_handleBackspace" oncommand="BrowserCommands.handleBackspace();" /> + <command id="cmd_handleShiftBackspace" oncommand="BrowserCommands.handleShiftBackspace();" /> - <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab({ event });"/> - <command id="cmd_newNavigatorTabNoEvent" oncommand="BrowserOpenTab();"/> - <command id="Browser:OpenFile" oncommand="BrowserOpenFileWindow();"/> + <command id="cmd_newNavigatorTab" oncommand="BrowserCommands.openTab({ event });"/> + <command id="cmd_newNavigatorTabNoEvent" oncommand="BrowserCommands.openTab();"/> + <command id="Browser:OpenFile" oncommand="BrowserCommands.openFileWindow();"/> <command id="Browser:SavePage" oncommand="saveBrowser(gBrowser.selectedBrowser);"/> <command id="Browser:SendLink" @@ -32,17 +32,17 @@ <command id="cmd_printPreviewToggle" oncommand="PrintUtils.togglePrintPreview(gBrowser.selectedBrowser.browsingContext);"/> <command id="cmd_file_importFromAnotherBrowser" oncommand="MigrationUtils.showMigrationWizard(window, { entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.FILE_MENU });"/> <command id="cmd_help_importFromAnotherBrowser" oncommand="MigrationUtils.showMigrationWizard(window, { entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.HELP_MENU });"/> - <command id="cmd_close" oncommand="BrowserCloseTabOrWindow(event);"/> - <command id="cmd_closeWindow" oncommand="BrowserTryToCloseWindow(event)"/> + <command id="cmd_close" oncommand="BrowserCommands.closeTabOrWindow(event);"/> + <command id="cmd_closeWindow" oncommand="BrowserCommands.tryToCloseWindow(event)"/> <command id="cmd_toggleMute" oncommand="gBrowser.toggleMuteAudioOnMultiSelectedTabs(gBrowser.selectedTab)"/> <command id="cmd_CustomizeToolbars" oncommand="gCustomizeMode.enter()"/> <command id="cmd_toggleOfflineStatus" oncommand="BrowserOffline.toggleOfflineStatus();"/> <command id="cmd_quitApplication" oncommand="goQuitApplication(event)"/> <command id="View:AboutProcesses" oncommand="switchToTabHavingURI('about:processes', true)"/> - <command id="View:PageSource" oncommand="BrowserViewSource(window.gBrowser.selectedBrowser);"/> - <command id="View:PageInfo" oncommand="BrowserPageInfo();"/> - <command id="View:FullScreen" oncommand="BrowserFullScreen();"/> + <command id="View:PageSource" oncommand="BrowserCommands.viewSource(window.gBrowser.selectedBrowser);"/> + <command id="View:PageInfo" oncommand="BrowserCommands.pageInfo();"/> + <command id="View:FullScreen" oncommand="BrowserCommands.fullScreen();"/> <command id="View:ReaderView" oncommand="AboutReaderParent.toggleReaderMode(event);"/> <command id="View:PictureInPicture" oncommand="PictureInPicture.onCommand(event);"/> <command id="cmd_find" oncommand="gLazyFindCommand('onFindCommand')"/> @@ -60,20 +60,20 @@ oncommand="PlacesCommandHook.searchBookmarks();"/> <command id="Browser:BookmarkAllTabs" oncommand="PlacesCommandHook.bookmarkTabs();"/> - <command id="Browser:Back" oncommand="BrowserBack();" disabled="true"/> - <command id="Browser:BackOrBackDuplicate" oncommand="BrowserBack(event);" disabled="true"> + <command id="Browser:Back" oncommand="BrowserCommands.back();" disabled="true"/> + <command id="Browser:BackOrBackDuplicate" oncommand="BrowserCommands.back(event);" disabled="true"> <observes element="Browser:Back" attribute="disabled"/> </command> - <command id="Browser:Forward" oncommand="BrowserForward();" disabled="true"/> - <command id="Browser:ForwardOrForwardDuplicate" oncommand="BrowserForward(event);" disabled="true"> + <command id="Browser:Forward" oncommand="BrowserCommands.forward();" disabled="true"/> + <command id="Browser:ForwardOrForwardDuplicate" oncommand="BrowserCommands.forward(event);" disabled="true"> <observes element="Browser:Forward" attribute="disabled"/> </command> - <command id="Browser:Stop" oncommand="BrowserStop();" disabled="true"/> - <command id="Browser:Reload" oncommand="if (event.shiftKey) BrowserReloadSkipCache(); else BrowserReload()" disabled="true"/> - <command id="Browser:ReloadOrDuplicate" oncommand="BrowserReloadOrDuplicate(event)" disabled="true"> + <command id="Browser:Stop" oncommand="BrowserCommands.stop();" disabled="true"/> + <command id="Browser:Reload" oncommand="if (event.shiftKey) BrowserCommands.reloadSkipCache(); else BrowserCommands.reload()" disabled="true"/> + <command id="Browser:ReloadOrDuplicate" oncommand="BrowserCommands.reloadOrDuplicate(event)" disabled="true"> <observes element="Browser:Reload" attribute="disabled"/> </command> - <command id="Browser:ReloadSkipCache" oncommand="BrowserReloadSkipCache()" disabled="true"> + <command id="Browser:ReloadSkipCache" oncommand="BrowserCommands.reloadSkipCache()" disabled="true"> <observes element="Browser:Reload" attribute="disabled"/> </command> <command id="Browser:NextTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(1, true);"/> @@ -216,7 +216,7 @@ <key id="goBackKb2" data-l10n-id="nav-back-shortcut-alt" command="Browser:Back" modifiers="accel"/> <key id="goForwardKb2" data-l10n-id="nav-fwd-shortcut-alt" command="Browser:Forward" modifiers="accel"/> #endif - <key id="goHome" keycode="VK_HOME" oncommand="BrowserHome();" modifiers="alt"/> + <key id="goHome" keycode="VK_HOME" oncommand="BrowserCommands.home();" modifiers="alt"/> <key keycode="VK_F5" command="Browser:Reload"/> #ifndef XP_MACOSX <key id="showAllHistoryKb" data-l10n-id="history-show-all-shortcut" command="Browser:ShowAllHistory" modifiers="accel,shift"/> diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js index a2a5f6ff71..60bd4fc01c 100644 --- a/browser/base/content/browser-siteIdentity.js +++ b/browser/base/content/browser-siteIdentity.js @@ -456,7 +456,9 @@ var gIdentityHandler = { ); // Reload the page with the content unblocked - BrowserReloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE); + BrowserCommands.reloadWithFlags( + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE + ); if (this._popupInitialized) { PanelMultiView.hidePopup(this._identityPopup); } @@ -475,7 +477,7 @@ var gIdentityHandler = { "mixed-content" ); if (reload) { - BrowserReload(); + BrowserCommands.reload(); } if (this._popupInitialized) { PanelMultiView.hidePopup(this._identityPopup); @@ -496,7 +498,7 @@ var gIdentityHandler = { port, gBrowser.contentPrincipal.originAttributes ); - BrowserReloadSkipCache(); + BrowserCommands.reloadSkipCache(); if (this._popupInitialized) { PanelMultiView.hidePopup(this._identityPopup); } @@ -611,7 +613,7 @@ var gIdentityHandler = { // Because "off" is 1 and "off temporarily" is 2, we can just check if the // sum of newValue and oldValue is 3. if (newValue + oldValue !== 3) { - BrowserReloadSkipCache(); + BrowserCommands.reloadSkipCache(); if (this._popupInitialized) { PanelMultiView.hidePopup(this._identityPopup); } @@ -1260,7 +1262,7 @@ var gIdentityHandler = { } }, - handleEvent(event) { + handleEvent() { let elem = document.activeElement; let position = elem.compareDocumentPosition(this._identityPopup); @@ -1278,7 +1280,7 @@ var gIdentityHandler = { } }, - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "perm-changed": { // Exclude permissions which do not appear in the UI in order to avoid diff --git a/browser/base/content/browser-sitePermissionPanel.js b/browser/base/content/browser-sitePermissionPanel.js index d81b636668..d977653f51 100644 --- a/browser/base/content/browser-sitePermissionPanel.js +++ b/browser/base/content/browser-sitePermissionPanel.js @@ -353,7 +353,7 @@ var gPermissionPanel = { } }, - handleEvent(event) { + handleEvent() { let elem = document.activeElement; let position = elem.compareDocumentPosition(this._permissionPopup); @@ -371,7 +371,7 @@ var gPermissionPanel = { } }, - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "fullscreen-painted": { if (subject != window || !this._exitedEventReceived) { diff --git a/browser/base/content/browser-siteProtections.js b/browser/base/content/browser-siteProtections.js index c44b4d3e8e..043dc53b95 100644 --- a/browser/base/content/browser-siteProtections.js +++ b/browser/base/content/browser-siteProtections.js @@ -31,8 +31,6 @@ class ProtectionCategory { * @param {Object} options - Category options. * @param {string} options.prefEnabled - ID of pref which controls the * category enabled state. - * @param {string} [options.l10nId] - Identifier l10n strings are keyed under - * for this category. Defaults to protection ID. * @param {Object} flags - Flags for this category to look for in the content * blocking event and content blocking log. * @param {Number} [flags.load] - Load flag for this protection category. If @@ -49,7 +47,7 @@ class ProtectionCategory { */ constructor( id, - { prefEnabled, l10nId }, + { prefEnabled }, { load, block, @@ -416,7 +414,6 @@ let TrackingProtection = super( "trackers", { - l10nId: "trackingContent", prefEnabled: "privacy.trackingprotection.enabled", }, { @@ -1065,7 +1062,6 @@ let SocialTracking = super( "socialblock", { - l10nId: "socialMediaTrackers", prefEnabled: "privacy.socialtracking.block_cookies.enabled", }, { @@ -1641,42 +1637,42 @@ var gProtectionsHandler = { ); }, - async showTrackersSubview(event) { + async showTrackersSubview() { await TrackingProtection.updateSubView(); this._protectionsPopupMultiView.showSubView( "protections-popup-trackersView" ); }, - async showSocialblockerSubview(event) { + async showSocialblockerSubview() { await SocialTracking.updateSubView(); this._protectionsPopupMultiView.showSubView( "protections-popup-socialblockView" ); }, - async showCookiesSubview(event) { + async showCookiesSubview() { await ThirdPartyCookies.updateSubView(); this._protectionsPopupMultiView.showSubView( "protections-popup-cookiesView" ); }, - async showFingerprintersSubview(event) { + async showFingerprintersSubview() { await Fingerprinting.updateSubView(); this._protectionsPopupMultiView.showSubView( "protections-popup-fingerprintersView" ); }, - async showCryptominersSubview(event) { + async showCryptominersSubview() { await Cryptomining.updateSubView(); this._protectionsPopupMultiView.showSubView( "protections-popup-cryptominersView" ); }, - async onCookieBannerClick(event) { + async onCookieBannerClick() { if (!cookieBannerHandling.isSiteSupported) { return; } @@ -2055,7 +2051,7 @@ var gProtectionsHandler = { } }, - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "browser:purge-session-history": // We need to update the earliest recorded date if history has been @@ -2194,7 +2190,7 @@ var gProtectionsHandler = { ContentBlockingAllowList.add(gBrowser.selectedBrowser); if (shouldReload) { this._hidePopup(); - BrowserReload(); + BrowserCommands.reload(); } }, @@ -2202,11 +2198,11 @@ var gProtectionsHandler = { ContentBlockingAllowList.remove(gBrowser.selectedBrowser); if (shouldReload) { this._hidePopup(); - BrowserReload(); + BrowserCommands.reload(); } }, - async onTPSwitchCommand(event) { + async onTPSwitchCommand() { // When the switch is clicked, we wait 500ms and then disable/enable // protections, causing the page to refresh, and close the popup. // We need to ensure we don't handle more clicks during the 500ms delay, @@ -2533,12 +2529,12 @@ var gProtectionsHandler = { }; const doc = event.target.ownerDocument; - const container = doc.getElementById("messaging-system-message-container"); + const container = doc.getElementById("info-message-container"); const infoButton = doc.getElementById("protections-popup-info-button"); const panelContainer = doc.getElementById("protections-popup"); const toggleMessage = () => { const learnMoreLink = doc.querySelector( - "#messaging-system-message-container .text-link" + "#info-message-container .text-link" ); if (learnMoreLink) { container.toggleAttribute("disabled"); diff --git a/browser/base/content/browser-sync.js b/browser/base/content/browser-sync.js index a94bf2b896..065546d7b8 100644 --- a/browser/base/content/browser-sync.js +++ b/browser/base/content/browser-sync.js @@ -55,7 +55,7 @@ this.SyncedTabsPanelList = class SyncedTabsPanelList { this.createSyncedTabs(); } - observe(subject, topic, data) { + observe(subject, topic) { if (topic == SyncedTabs.TOPIC_TABS_CHANGED) { this._showSyncedTabs(); } @@ -648,7 +648,7 @@ var gSync = { // shows the device list will start with `recentDeviceList`, but should also // force a refresh, both of which should mean in the worst-case, the UI is up // to date after a very short delay. - async ensureFxaDevices(options) { + async ensureFxaDevices() { if (UIState.get().status != UIState.STATUS_SIGNED_IN) { console.info("Skipping device list refresh; not signed in"); return; @@ -768,7 +768,7 @@ var gSync = { } } - item.addEventListener("command", event => { + item.addEventListener("command", () => { if (panelNode) { PanelMultiView.hidePopup(panelNode); } @@ -1364,7 +1364,7 @@ var gSync = { return; } if (!createDeviceNodeFn) { - createDeviceNodeFn = (targetId, name, targetType, lastModified) => { + createDeviceNodeFn = (targetId, name) => { let eltName = name ? "menuitem" : "menuseparator"; return document.createXULElement(eltName); }; @@ -1462,7 +1462,7 @@ var gSync = { fxAccounts.flushLogFile(); }); }; - const onSendAllCommand = event => { + const onSendAllCommand = () => { send(targets); }; const onTargetDeviceCommand = event => { diff --git a/browser/base/content/browser-tabsintitlebar.js b/browser/base/content/browser-tabsintitlebar.js index caf9986b2f..d7f71ed450 100644 --- a/browser/base/content/browser-tabsintitlebar.js +++ b/browser/base/content/browser-tabsintitlebar.js @@ -43,7 +43,7 @@ var TabsInTitlebar = { return document.documentElement.getAttribute("tabsintitlebar") == "true"; }, - observe(subject, topic, data) { + observe(subject, topic) { if (topic == "nsPref:changed") { this._readPref(); } diff --git a/browser/base/content/browser-thumbnails.js b/browser/base/content/browser-thumbnails.js index 1162914ddf..2ca6148b67 100644 --- a/browser/base/content/browser-thumbnails.js +++ b/browser/base/content/browser-thumbnails.js @@ -103,7 +103,7 @@ var gBrowserThumbnails = { ChromeUtils.defineLazyGetter(this, "_topSiteURLs", getTopSiteURLs); }, - notify: function Thumbnails_notify(timer) { + notify: function Thumbnails_notify() { gBrowserThumbnails._topSiteURLsRefreshTimer = null; gBrowserThumbnails.clearTopSiteURLCache(); }, @@ -116,7 +116,7 @@ var gBrowserThumbnails = { aWebProgress, aRequest, aStateFlags, - aStatus + _aStatus ) { if ( aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && diff --git a/browser/base/content/browser-toolbarKeyNav.js b/browser/base/content/browser-toolbarKeyNav.js index caa01100c5..c65d99f6f0 100644 --- a/browser/base/content/browser-toolbarKeyNav.js +++ b/browser/base/content/browser-toolbarKeyNav.js @@ -137,7 +137,7 @@ ToolbarKeyboardNavigator = { }, // CustomizableUI event handler - onWidgetAdded(aWidgetId, aArea, aPosition) { + onWidgetAdded(aWidgetId, aArea) { if (!this.kToolbars.includes(aArea)) { return; } diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index c9ebddb7f5..6e776a9ce7 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -250,8 +250,7 @@ toolbar[customizing] > .overflow-button { display: none; } -toolbar[customizing] #ion-button, -toolbar[customizing] #whats-new-menu-button { +toolbar[customizing] #ion-button { display: none; } @@ -382,7 +381,7 @@ toolbarpaletteitem { toolbar[brighttext] & { list-style-image: var(--webextension-toolbar-image-light, inherit); } - toolbar:not([brighttext]) &:-moz-lwtheme { + :root[lwtheme] toolbar:not([brighttext]) & { list-style-image: var(--webextension-toolbar-image-dark, inherit); } toolbaritem:is([overflowedItem="true"], [cui-areatype="panel"]) > & { @@ -480,12 +479,6 @@ toolbar:not(#TabsToolbar) > #personal-bookmarks { flex: 1; } -@media (-moz-platform: macos) { - :root[inFullscreen="true"] { - padding-top: 0; /* override drawintitlebar="true" */ - } -} - /* Hide menu elements intended for keyboard access support */ #main-menubar[openedwithkey=false] .show-only-for-keyboard { display: none; @@ -916,7 +909,7 @@ menupopup[emptyplacesresult="true"] > .hide-if-empty-places-result { position: absolute; } -browser[tabmodalPromptShowing], browser[tabDialogShowing] { +browser[tabDialogShowing] { -moz-user-focus: none !important; } diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index c91a5d4db2..72753da622 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -79,7 +79,6 @@ ChromeUtils.defineESModuleGetters(this, { SubDialog: "resource://gre/modules/SubDialog.sys.mjs", SubDialogManager: "resource://gre/modules/SubDialog.sys.mjs", TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs", - TabModalPrompt: "chrome://global/content/tabprompts.sys.mjs", TabsSetupFlowManager: "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs", TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", @@ -109,6 +108,12 @@ ChromeUtils.defineLazyGetter(this, "fxAccounts", () => { XPCOMUtils.defineLazyScriptGetter( this, + ["BrowserCommands", "kSkipCacheFlags"], + "chrome://browser/content/browser-commands.js" +); + +XPCOMUtils.defineLazyScriptGetter( + this, "PlacesTreeView", "chrome://browser/content/places/treeView.js" ); @@ -546,7 +551,7 @@ XPCOMUtils.defineLazyPreferenceGetter( "gFxaToolbarAccessed", "identity.fxaccounts.toolbar.accessed", false, - (aPref, aOldVal, aNewVal) => { + () => { updateFxaToolbarMenu(gFxaToolbarEnabled); } ); @@ -691,7 +696,6 @@ function shouldSuppressPopupNotifications() { // don't cover up the prompt. return ( window.windowState == window.STATE_MINIMIZED || - gBrowser?.selectedBrowser.hasAttribute("tabmodalChromePromptShowing") || gBrowser?.selectedBrowser.hasAttribute("tabDialogShowing") || gDialogBox?.isOpen ); @@ -1019,7 +1023,7 @@ const gClickAndHoldListenersOnElement = { }; const gSessionHistoryObserver = { - observe(subject, topic, data) { + observe(subject, topic) { if (topic != "browser:purge-session-history") { return; } @@ -1037,7 +1041,7 @@ const gSessionHistoryObserver = { const gStoragePressureObserver = { _lastNotificationTime: -1, - async observe(subject, topic, data) { + async observe(subject, topic) { if (topic != "QuotaManager::StoragePressure") { return; } @@ -1088,7 +1092,7 @@ const gStoragePressureObserver = { document.l10n.setAttributes(message, "space-alert-over-5gb-message2"); buttons.push({ "l10n-id": "space-alert-over-5gb-settings-button", - callback(notificationBar, button) { + callback() { // The advanced subpanes are only supported in the old organization, which will // be removed by bug 1349689. openPreferences("privacy-sitedata"); @@ -1475,7 +1479,7 @@ var gKeywordURIFixup = { ); }, - observe(fixupInfo, topic, data) { + observe(fixupInfo) { fixupInfo.QueryInterface(Ci.nsIURIFixupInfo); let browser = fixupInfo.consumer?.top?.embedderElement; @@ -2351,13 +2355,6 @@ var gBrowserInit = { }); scheduleIdleTask(() => { - // load the tab preview component - import("chrome://browser/content/tabpreview/tabpreview.mjs").catch( - console.error - ); - }); - - scheduleIdleTask(() => { // setup simple gestures support gGestureSupport.init(true); @@ -2620,17 +2617,17 @@ gBrowserInit.idleTasksFinishedPromise = new Promise(resolve => { function HandleAppCommandEvent(evt) { switch (evt.command) { case "Back": - BrowserBack(); + BrowserCommands.back(); break; case "Forward": - BrowserForward(); + BrowserCommands.forward(); break; case "Reload": - BrowserReloadSkipCache(); + BrowserCommands.reloadSkipCache(); break; case "Stop": if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") { - BrowserStop(); + BrowserCommands.stop(); } break; case "Search": @@ -2640,13 +2637,13 @@ function HandleAppCommandEvent(evt) { SidebarUI.toggle("viewBookmarksSidebar"); break; case "Home": - BrowserHome(); + BrowserCommands.home(); break; case "New": - BrowserOpenTab(); + BrowserCommands.openTab(); break; case "Close": - BrowserCloseTabOrWindow(); + BrowserCommands.closeTabOrWindow(); break; case "Find": gLazyFindCommand("onFindCommand"); @@ -2655,7 +2652,7 @@ function HandleAppCommandEvent(evt) { openHelpLink("firefox-help"); break; case "Open": - BrowserOpenFileWindow(); + BrowserCommands.openFileWindow(); break; case "Print": PrintUtils.startPrintWindow(gBrowser.selectedBrowser.browsingContext); @@ -2673,203 +2670,6 @@ function HandleAppCommandEvent(evt) { evt.preventDefault(); } -function gotoHistoryIndex(aEvent) { - aEvent = getRootEvent(aEvent); - - let index = aEvent.target.getAttribute("index"); - if (!index) { - return false; - } - - let where = whereToOpenLink(aEvent); - - if (where == "current") { - // Normal click. Go there in the current tab and update session history. - - try { - gBrowser.gotoIndex(index); - } catch (ex) { - return false; - } - return true; - } - // Modified click. Go there in a new tab/window. - - let historyindex = aEvent.target.getAttribute("historyindex"); - duplicateTabIn(gBrowser.selectedTab, where, Number(historyindex)); - return true; -} - -function BrowserForward(aEvent) { - let where = whereToOpenLink(aEvent, false, true); - - if (where == "current") { - try { - gBrowser.goForward(); - } catch (ex) {} - } else { - duplicateTabIn(gBrowser.selectedTab, where, 1); - } -} - -function BrowserBack(aEvent) { - let where = whereToOpenLink(aEvent, false, true); - - if (where == "current") { - try { - gBrowser.goBack(); - } catch (ex) {} - } else { - duplicateTabIn(gBrowser.selectedTab, where, -1); - } -} - -function BrowserHandleBackspace() { - switch (Services.prefs.getIntPref("browser.backspace_action")) { - case 0: - BrowserBack(); - break; - case 1: - goDoCommand("cmd_scrollPageUp"); - break; - } -} - -function BrowserHandleShiftBackspace() { - switch (Services.prefs.getIntPref("browser.backspace_action")) { - case 0: - BrowserForward(); - break; - case 1: - goDoCommand("cmd_scrollPageDown"); - break; - } -} - -function BrowserStop() { - gBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL); -} - -function BrowserReloadOrDuplicate(aEvent) { - aEvent = getRootEvent(aEvent); - let accelKeyPressed = - AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey; - var backgroundTabModifier = aEvent.button == 1 || accelKeyPressed; - - if (aEvent.shiftKey && !backgroundTabModifier) { - BrowserReloadSkipCache(); - return; - } - - let where = whereToOpenLink(aEvent, false, true); - if (where == "current") { - BrowserReload(); - } else { - duplicateTabIn(gBrowser.selectedTab, where); - } -} - -function BrowserReload() { - if (gBrowser.currentURI.schemeIs("view-source")) { - // Bug 1167797: For view source, we always skip the cache - return BrowserReloadSkipCache(); - } - const reloadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; - BrowserReloadWithFlags(reloadFlags); -} - -const kSkipCacheFlags = - Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | - Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; -function BrowserReloadSkipCache() { - // Bypass proxy and cache. - BrowserReloadWithFlags(kSkipCacheFlags); -} - -function BrowserHome(aEvent) { - if (aEvent && "button" in aEvent && aEvent.button == 2) { - // right-click: do nothing - return; - } - - var homePage = HomePage.get(window); - var where = whereToOpenLink(aEvent, false, true); - var urls; - var notifyObservers; - - // Don't load the home page in pinned or hidden tabs (e.g. Firefox View). - if ( - where == "current" && - (gBrowser?.selectedTab.pinned || gBrowser?.selectedTab.hidden) - ) { - where = "tab"; - } - - // openTrustedLinkIn in utilityOverlay.js doesn't handle loading multiple pages - switch (where) { - case "current": - // If we're going to load an initial page in the current tab as the - // home page, we set initialPageLoadedFromURLBar so that the URL - // bar is cleared properly (even during a remoteness flip). - if (isInitialPage(homePage)) { - gBrowser.selectedBrowser.initialPageLoadedFromUserAction = homePage; - } - loadOneOrMoreURIs( - homePage, - Services.scriptSecurityManager.getSystemPrincipal(), - null - ); - if (isBlankPageURL(homePage)) { - gURLBar.select(); - } else { - gBrowser.selectedBrowser.focus(); - } - notifyObservers = true; - aEvent?.preventDefault(); - break; - case "tabshifted": - case "tab": - urls = homePage.split("|"); - var loadInBackground = Services.prefs.getBoolPref( - "browser.tabs.loadBookmarksInBackground", - false - ); - // The homepage observer event should only be triggered when the homepage opens - // in the foreground. This is mostly to support the homepage changed by extension - // doorhanger which doesn't currently support background pages. This may change in - // bug 1438396. - notifyObservers = !loadInBackground; - gBrowser.loadTabs(urls, { - inBackground: loadInBackground, - triggeringPrincipal: - Services.scriptSecurityManager.getSystemPrincipal(), - csp: null, - }); - if (!loadInBackground) { - if (isBlankPageURL(homePage)) { - gURLBar.select(); - } else { - gBrowser.selectedBrowser.focus(); - } - } - aEvent?.preventDefault(); - break; - case "window": - // OpenBrowserWindow will trigger the observer event, so no need to do so here. - notifyObservers = false; - OpenBrowserWindow(); - aEvent?.preventDefault(); - break; - } - if (notifyObservers) { - // A notification for when a user has triggered their homepage. This is used - // to display a doorhanger explaining that an extension has modified the - // homepage, if necessary. Observers are only notified if the homepage - // becomes the active page. - Services.obs.notifyObservers(null, "browser-open-homepage-start"); - } -} - function loadOneOrMoreURIs(aURIString, aTriggeringPrincipal, aCsp) { // we're not a browser window, pass the URI string to a new browser window if (window.location.href != AppConstants.BROWSER_CHROME_URL) { @@ -2918,62 +2718,6 @@ function openLocation(event) { ); } -function BrowserOpenTab({ event, url } = {}) { - let werePassedURL = !!url; - url ??= BROWSER_NEW_TAB_URL; - let searchClipboard = gMiddleClickNewTabUsesPasteboard && event?.button == 1; - - let relatedToCurrent = false; - let where = "tab"; - - if (event) { - where = whereToOpenLink(event, false, true); - - switch (where) { - case "tab": - case "tabshifted": - // When accel-click or middle-click are used, open the new tab as - // related to the current tab. - relatedToCurrent = true; - break; - case "current": - where = "tab"; - break; - } - } - - // A notification intended to be useful for modular peformance tracking - // starting as close as is reasonably possible to the time when the user - // expressed the intent to open a new tab. Since there are a lot of - // entry points, this won't catch every single tab created, but most - // initiated by the user should go through here. - // - // Note 1: This notification gets notified with a promise that resolves - // with the linked browser when the tab gets created - // Note 2: This is also used to notify a user that an extension has changed - // the New Tab page. - Services.obs.notifyObservers( - { - wrappedJSObject: new Promise(resolve => { - let options = { - relatedToCurrent, - resolveOnNewTabCreated: resolve, - }; - if (!werePassedURL && searchClipboard) { - let clipboard = readFromClipboard(); - clipboard = UrlbarUtils.stripUnsafeProtocolOnPaste(clipboard).trim(); - if (clipboard) { - url = clipboard; - options.allowThirdPartyFixup = true; - } - } - openTrustedLinkIn(url, where, options); - }), - }, - "browser-open-newtab-start" - ); -} - var gLastOpenDirectory = { _lastDir: null, get path() { @@ -3014,76 +2758,6 @@ var gLastOpenDirectory = { }, }; -function BrowserOpenFileWindow() { - // Get filepicker component. - try { - const nsIFilePicker = Ci.nsIFilePicker; - let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); - let fpCallback = function fpCallback_done(aResult) { - if (aResult == nsIFilePicker.returnOK) { - try { - if (fp.file) { - gLastOpenDirectory.path = fp.file.parent.QueryInterface(Ci.nsIFile); - } - } catch (ex) {} - openTrustedLinkIn(fp.fileURL.spec, "current"); - } - }; - - fp.init( - window.browsingContext, - gNavigatorBundle.getString("openFile"), - nsIFilePicker.modeOpen - ); - fp.appendFilters( - nsIFilePicker.filterAll | - nsIFilePicker.filterText | - nsIFilePicker.filterImages | - nsIFilePicker.filterXML | - nsIFilePicker.filterHTML | - nsIFilePicker.filterPDF - ); - fp.displayDirectory = gLastOpenDirectory.path; - fp.open(fpCallback); - } catch (ex) {} -} - -function BrowserCloseTabOrWindow(event) { - // If we're not a browser window, just close the window. - if (window.location.href != AppConstants.BROWSER_CHROME_URL) { - closeWindow(true); - return; - } - - // In a multi-select context, close all selected tabs - if (gBrowser.multiSelectedTabsCount) { - gBrowser.removeMultiSelectedTabs(); - return; - } - - // Keyboard shortcuts that would close a tab that is pinned select the first - // unpinned tab instead. - if ( - event && - (event.ctrlKey || event.metaKey || event.altKey) && - gBrowser.selectedTab.pinned - ) { - if (gBrowser.visibleTabs.length > gBrowser._numPinnedTabs) { - gBrowser.tabContainer.selectedIndex = gBrowser._numPinnedTabs; - } - return; - } - - // If the current tab is the last one, this will close the window. - gBrowser.removeCurrentTab({ animate: true }); -} - -function BrowserTryToCloseWindow(event) { - if (WindowIsClosing(event)) { - window.close(); - } // WindowIsClosing does all the necessary checks -} - function getLoadContext() { return window.docShell.QueryInterface(Ci.nsILoadContext); } @@ -3120,162 +2794,6 @@ function readFromClipboard() { return url; } -/** - * Open the View Source dialog. - * - * @param args - * An object with the following properties: - * - * URL (required): - * A string URL for the page we'd like to view the source of. - * browser (optional): - * The browser containing the document that we would like to view the - * source of. This is required if outerWindowID is passed. - * outerWindowID (optional): - * The outerWindowID of the content window containing the document that - * we want to view the source of. You only need to provide this if you - * want to attempt to retrieve the document source from the network - * cache. - * lineNumber (optional): - * The line number to focus on once the source is loaded. - */ -async function BrowserViewSourceOfDocument(args) { - // Check if external view source is enabled. If so, try it. If it fails, - // fallback to internal view source. - if (Services.prefs.getBoolPref("view_source.editor.external")) { - try { - await top.gViewSourceUtils.openInExternalEditor(args); - return; - } catch (data) {} - } - - let tabBrowser = gBrowser; - let preferredRemoteType; - let initialBrowsingContextGroupId; - if (args.browser) { - preferredRemoteType = args.browser.remoteType; - initialBrowsingContextGroupId = args.browser.browsingContext.group.id; - } else { - if (!tabBrowser) { - throw new Error( - "BrowserViewSourceOfDocument should be passed the " + - "subject browser if called from a window without " + - "gBrowser defined." - ); - } - // Some internal URLs (such as specific chrome: and about: URLs that are - // not yet remote ready) cannot be loaded in a remote browser. View - // source in tab expects the new view source browser's remoteness to match - // that of the original URL, so disable remoteness if necessary for this - // URL. - var oa = E10SUtils.predictOriginAttributes({ window }); - preferredRemoteType = E10SUtils.getRemoteTypeForURI( - args.URL, - gMultiProcessBrowser, - gFissionBrowser, - E10SUtils.DEFAULT_REMOTE_TYPE, - null, - oa - ); - } - - // In the case of popups, we need to find a non-popup browser window. - if (!tabBrowser || !window.toolbar.visible) { - // This returns only non-popup browser windows by default. - let browserWindow = BrowserWindowTracker.getTopWindow(); - tabBrowser = browserWindow.gBrowser; - } - - const inNewWindow = !Services.prefs.getBoolPref("view_source.tab"); - - // `viewSourceInBrowser` will load the source content from the page - // descriptor for the tab (when possible) or fallback to the network if - // that fails. Either way, the view source module will manage the tab's - // location, so use "about:blank" here to avoid unnecessary redundant - // requests. - let tab = tabBrowser.addTab("about:blank", { - relatedToCurrent: true, - inBackground: inNewWindow, - skipAnimation: inNewWindow, - preferredRemoteType, - initialBrowsingContextGroupId, - triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), - skipLoad: true, - }); - args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab); - top.gViewSourceUtils.viewSourceInBrowser(args); - - if (inNewWindow) { - tabBrowser.hideTab(tab); - tabBrowser.replaceTabWithWindow(tab); - } -} - -/** - * Opens the View Source dialog for the source loaded in the root - * top-level document of the browser. This is really just a - * convenience wrapper around BrowserViewSourceOfDocument. - * - * @param browser - * The browser that we want to load the source of. - */ -function BrowserViewSource(browser) { - BrowserViewSourceOfDocument({ - browser, - outerWindowID: browser.outerWindowID, - URL: browser.currentURI.spec, - }); -} - -// documentURL - URL of the document to view, or null for this window's document -// initialTab - name of the initial tab to display, or null for the first tab -// imageElement - image to load in the Media Tab of the Page Info window; can be null/omitted -// browsingContext - the browsingContext of the frame that we want to view information about; can be null/omitted -// browser - the browser containing the document we're interested in inspecting; can be null/omitted -function BrowserPageInfo( - documentURL, - initialTab, - imageElement, - browsingContext, - browser -) { - let args = { initialTab, imageElement, browsingContext, browser }; - - documentURL = documentURL || window.gBrowser.selectedBrowser.currentURI.spec; - - let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window); - - // Check for windows matching the url - for (let currentWindow of Services.wm.getEnumerator("Browser:page-info")) { - if (currentWindow.closed) { - continue; - } - if ( - currentWindow.document.documentElement.getAttribute("relatedUrl") == - documentURL && - PrivateBrowsingUtils.isWindowPrivate(currentWindow) == isPrivate - ) { - currentWindow.focus(); - currentWindow.resetPageInfo(args); - return currentWindow; - } - } - - // We didn't find a matching window, so open a new one. - let options = "chrome,toolbar,dialog=no,resizable"; - - // Ensure the window groups correctly in the Windows taskbar - if (isPrivate) { - options += ",private"; - } - return openDialog( - "chrome://browser/content/pageinfo/pageInfo.xhtml", - "", - options, - args - ); -} - function UpdateUrlbarSearchSplitterState() { var splitter = document.getElementById("urlbar-search-splitter"); var urlbar = document.getElementById("urlbar-container"); @@ -3480,88 +2998,6 @@ function getDefaultHomePage() { return url; } -function BrowserFullScreen() { - window.fullScreen = !window.fullScreen || BrowserHandler.kiosk; -} - -function BrowserReloadWithFlags(reloadFlags) { - let unchangedRemoteness = []; - - for (let tab of gBrowser.selectedTabs) { - let browser = tab.linkedBrowser; - let url = browser.currentURI; - let urlSpec = url.spec; - // We need to cache the content principal here because the browser will be - // reconstructed when the remoteness changes and the content prinicpal will - // be cleared after reconstruction. - let principal = tab.linkedBrowser.contentPrincipal; - if (gBrowser.updateBrowserRemotenessByURL(browser, urlSpec)) { - // If the remoteness has changed, the new browser doesn't have any - // information of what was loaded before, so we need to load the previous - // URL again. - if (tab.linkedPanel) { - loadBrowserURI(browser, url, principal); - } else { - // Shift to fully loaded browser and make - // sure load handler is instantiated. - tab.addEventListener( - "SSTabRestoring", - () => loadBrowserURI(browser, url, principal), - { once: true } - ); - gBrowser._insertBrowser(tab); - } - } else { - unchangedRemoteness.push(tab); - } - } - - if (!unchangedRemoteness.length) { - return; - } - - // Reset temporary permissions on the remaining tabs to reload. - // This is done here because we only want to reset - // permissions on user reload. - for (let tab of unchangedRemoteness) { - SitePermissions.clearTemporaryBlockPermissions(tab.linkedBrowser); - // Also reset DOS mitigations for the basic auth prompt on reload. - delete tab.linkedBrowser.authPromptAbuseCounter; - } - gIdentityHandler.hidePopup(); - gPermissionPanel.hidePopup(); - - let handlingUserInput = document.hasValidTransientUserGestureActivation; - - for (let tab of unchangedRemoteness) { - if (tab.linkedPanel) { - sendReloadMessage(tab); - } else { - // Shift to fully loaded browser and make - // sure load handler is instantiated. - tab.addEventListener("SSTabRestoring", () => sendReloadMessage(tab), { - once: true, - }); - gBrowser._insertBrowser(tab); - } - } - - function loadBrowserURI(browser, url, principal) { - browser.loadURI(url, { - flags: reloadFlags, - triggeringPrincipal: principal, - }); - } - - function sendReloadMessage(tab) { - tab.linkedBrowser.sendMessageToActor( - "Browser:Reload", - { flags: reloadFlags, handlingUserInput }, - "BrowserTab" - ); - } -} - // TODO: can we pull getPEMString in from pippki.js instead of // duplicating them here? function getPEMString(cert) { @@ -4053,7 +3489,7 @@ const BrowserSearch = { win.BrowserSearch.webSearch(); } else { // If there are no open browser windows, open a new one - var observer = function (subject, topic, data) { + var observer = function (subject) { if (subject == win) { BrowserSearch.webSearch(); Services.obs.removeObserver( @@ -4444,7 +3880,8 @@ function FillHistoryMenu(aParent) { item.setAttribute("label", entry.title || uri); item.setAttribute("index", j); - // Cache this so that gotoHistoryIndex doesn't need the original index + // Cache this so that BrowserCommands.gotoHistoryIndex doesn't need the + // original index item.setAttribute("historyindex", j - index); if (j != index) { @@ -5037,7 +4474,7 @@ var XULBrowserWindow = { this.setOverLink("", { hideStatusPanelImmediately: true }); }, - showTooltip(xDevPix, yDevPix, tooltip, direction, browser) { + showTooltip(xDevPix, yDevPix, tooltip, direction, _browser) { if ( Cc["@mozilla.org/widget/dragservice;1"] .getService(Ci.nsIDragService) @@ -5070,14 +4507,7 @@ var XULBrowserWindow = { return gBrowser.tabs.length; }, - onProgressChange( - aWebProgress, - aRequest, - aCurSelfProgress, - aMaxSelfProgress, - aCurTotalProgress, - aMaxTotalProgress - ) { + onProgressChange() { // Do nothing. }, @@ -5576,7 +5006,7 @@ var XULBrowserWindow = { // 2. Called by tabbrowser.xml when updating the current browser. // 3. Called directly during this object's initializations. // aRequest will be null always in case 2 and 3, and sometimes in case 1. - onSecurityChange(aWebProgress, aRequest, aState, aIsSimulated) { + onSecurityChange(aWebProgress, aRequest, aState, _aIsSimulated) { // Don't need to do anything if the data we use to update the UI hasn't // changed let uri = gBrowser.currentURI; @@ -5619,7 +5049,7 @@ var XULBrowserWindow = { aStateFlags, aStatus, aMessage, - aTotalProgress + _aTotalProgress ) { if (FullZoom.updateBackgroundTabs) { FullZoom.onLocationChange(gBrowser.currentURI, true); @@ -6087,7 +5517,7 @@ nsBrowserAccess.prototype = { } if (aIsExternal && (!aURI || aURI.spec == "about:blank")) { - win.BrowserOpenTab(); // this also focuses the location bar + win.BrowserCommands.openTab(); // this also focuses the location bar win.focus(); return win.gBrowser.selectedBrowser; } @@ -6801,7 +6231,7 @@ var gTabletModePageCounter = { }; function displaySecurityInfo() { - BrowserPageInfo(null, "securityTab"); + BrowserCommands.pageInfo(null, "securityTab"); } // Updates the UI density (for touch and compact mode) based on the uidensity pref. @@ -7299,7 +6729,9 @@ function handleDroppedLink( function BrowserForceEncodingDetection() { gBrowser.selectedBrowser.forceEncodingDetection(); - BrowserReloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); + BrowserCommands.reloadWithFlags( + Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE + ); } var ToolbarContextMenu = { @@ -7467,7 +6899,7 @@ var BrowserOffline = { }, // nsIObserver - observe(aSubject, aTopic, aState) { + observe(aSubject, aTopic) { if (aTopic != "network:offline-status-changed") { return; } @@ -7695,8 +7127,8 @@ var WebAuthnPromptHelper = { if (data.prompt.type == "presence") { this.presence_required(mgr, data); - } else if (data.prompt.type == "register-direct") { - this.registerDirect(mgr, data); + } else if (data.prompt.type == "attestation-consent") { + this.attestation_consent(mgr, data); } else if (data.prompt.type == "pin-required") { this.pin_required(mgr, false, data); } else if (data.prompt.type == "pin-invalid") { @@ -7815,7 +7247,7 @@ var WebAuthnPromptHelper = { secondaryActions.push({ label, accessKey: i.toString(), - callback(aState) { + callback() { mgr.selectionCallback(tid, i); }, }); @@ -7859,9 +7291,23 @@ var WebAuthnPromptHelper = { ); }, - registerDirect(mgr, { origin, tid }) { - let mainAction = this.buildProceedAction(mgr, tid); - let secondaryActions = [this.buildCancelAction(mgr, tid)]; + attestation_consent(mgr, { origin, tid }) { + let mainAction = { + label: gNavigatorBundle.getString("webauthn.allow"), + accessKey: gNavigatorBundle.getString("webauthn.allow.accesskey"), + callback(_state) { + mgr.setHasAttestationConsent(tid, true); + }, + }; + let secondaryActions = [ + { + label: gNavigatorBundle.getString("webauthn.block"), + accessKey: gNavigatorBundle.getString("webauthn.block.accesskey"), + callback(_state) { + mgr.setHasAttestationConsent(tid, false); + }, + }, + ]; let learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + @@ -7869,9 +7315,6 @@ var WebAuthnPromptHelper = { let options = { learnMoreURL, - checkbox: { - label: gNavigatorBundle.getString("webauthn.anonymize"), - }, hintText: "webauthn.registerDirectPromptHint", }; this.show( @@ -7994,16 +7437,6 @@ var WebAuthnPromptHelper = { } }, - buildProceedAction(mgr, tid) { - return { - label: gNavigatorBundle.getString("webauthn.proceed"), - accessKey: gNavigatorBundle.getString("webauthn.proceed.accesskey"), - callback(state) { - mgr.resumeMakeCredential(tid, state.checkboxChecked); - }, - }; - }, - buildCancelAction(mgr, tid) { return { label: gNavigatorBundle.getString("webauthn.cancel"), @@ -8220,7 +7653,7 @@ function BrowserOpenAddonsMgr(aView, { selectTabByViewId = false } = {}) { let emWindow; let browserWindow; - var receivePong = function (aSubject, aTopic, aData) { + var receivePong = function (aSubject) { let browserWin = aSubject.browsingContext.topChromeWindow; if (!emWindow || browserWin == window /* favor the current window */) { if ( @@ -8262,7 +7695,7 @@ function BrowserOpenAddonsMgr(aView, { selectTabByViewId = false } = {}) { switchToTabHavingURI("about:addons", true); } - Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.addObserver(function observer(aSubject, aTopic) { Services.obs.removeObserver(observer, aTopic); if (aView) { aSubject.loadView(aView); @@ -8449,7 +7882,7 @@ function ReportSiteIssue() { * and when the "remote-listening" system notification fires. */ const gRemoteControl = { - observe(subject, topic, data) { + observe() { gRemoteControl.updateVisualCue(); }, @@ -8658,7 +8091,7 @@ function switchToTabHavingURI( ignoreQueryString || replaceQueryString, ignoreFragmentWhenComparing ); - let browserUserContextId = browser.getAttribute("usercontextid"); + let browserUserContextId = browser.getAttribute("usercontextid") || ""; if (aUserContextId != null && aUserContextId != browserUserContextId) { continue; } @@ -9447,221 +8880,6 @@ TabDialogBox.prototype.QueryInterface = ChromeUtils.generateQI([ "nsISupportsWeakReference", ]); -function TabModalPromptBox(browser) { - this._weakBrowserRef = Cu.getWeakReference(browser); - /* - * These WeakMaps holds the TabModalPrompt instances, key to the <tabmodalprompt> prompt - * in the DOM. We don't want to hold the instances directly to avoid leaking. - * - * WeakMap also prevents us from reading back its insertion order. - * Order of the elements in the DOM should be the only order to consider. - */ - this._contentPrompts = new WeakMap(); - this._tabPrompts = new WeakMap(); -} - -TabModalPromptBox.prototype = { - _promptCloseCallback( - onCloseCallback, - principalToAllowFocusFor, - allowFocusCheckbox, - ...args - ) { - if ( - principalToAllowFocusFor && - allowFocusCheckbox && - allowFocusCheckbox.checked - ) { - Services.perms.addFromPrincipal( - principalToAllowFocusFor, - "focus-tab-by-prompt", - Services.perms.ALLOW_ACTION - ); - } - onCloseCallback.apply(this, args); - }, - - getPrompt(promptEl) { - if (promptEl.classList.contains("tab-prompt")) { - return this._tabPrompts.get(promptEl); - } - return this._contentPrompts.get(promptEl); - }, - - appendPrompt(args, onCloseCallback) { - let browser = this.browser; - let newPrompt = new TabModalPrompt(browser.ownerGlobal); - - if (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { - newPrompt.element.classList.add("tab-prompt"); - this._tabPrompts.set(newPrompt.element, newPrompt); - } else { - newPrompt.element.classList.add("content-prompt"); - this._contentPrompts.set(newPrompt.element, newPrompt); - } - - browser.parentNode.insertBefore( - newPrompt.element, - browser.nextElementSibling - ); - browser.setAttribute("tabmodalPromptShowing", true); - - // Indicate if a tab modal chrome prompt is being shown so that - // PopupNotifications are suppressed. - if ( - args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB && - !browser.hasAttribute("tabmodalChromePromptShowing") - ) { - browser.setAttribute("tabmodalChromePromptShowing", true); - // Notify popup notifications of the UI change so they hide their - // notification panels. - UpdatePopupNotificationsVisibility(); - } - - let prompts = this.listPrompts(args.modalType); - if (prompts.length > 1) { - // Let's hide ourself behind the current prompt. - newPrompt.element.hidden = true; - } - - let principalToAllowFocusFor = this._allowTabFocusByPromptPrincipal; - delete this._allowTabFocusByPromptPrincipal; - - let allowFocusCheckbox; // Define outside the if block so we can bind it into the callback. - let hostForAllowFocusCheckbox = ""; - try { - hostForAllowFocusCheckbox = principalToAllowFocusFor.URI.host; - } catch (ex) { - /* Ignore exceptions for host-less URIs */ - } - if (hostForAllowFocusCheckbox) { - let allowFocusRow = document.createElement("div"); - - let spacer = document.createElement("div"); - allowFocusRow.appendChild(spacer); - - allowFocusCheckbox = document.createXULElement("checkbox"); - document.l10n.setAttributes( - allowFocusCheckbox, - "tabbrowser-allow-dialogs-to-get-focus", - { domain: hostForAllowFocusCheckbox } - ); - allowFocusRow.appendChild(allowFocusCheckbox); - - newPrompt.ui.rows.append(allowFocusRow); - } - - let tab = gBrowser.getTabForBrowser(browser); - let closeCB = this._promptCloseCallback.bind( - null, - onCloseCallback, - principalToAllowFocusFor, - allowFocusCheckbox - ); - newPrompt.init(args, tab, closeCB); - return newPrompt; - }, - - removePrompt(aPrompt) { - let { modalType } = aPrompt.args; - if (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { - this._tabPrompts.delete(aPrompt.element); - } else { - this._contentPrompts.delete(aPrompt.element); - } - - let browser = this.browser; - aPrompt.element.remove(); - - let prompts = this.listPrompts(modalType); - if (prompts.length) { - let prompt = prompts[prompts.length - 1]; - prompt.element.hidden = false; - // Because we were hidden before, this won't have been possible, so do it now: - prompt.Dialog.setDefaultFocus(); - } else if (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { - // If we remove the last tab chrome prompt, also remove the browser - // attribute. - browser.removeAttribute("tabmodalChromePromptShowing"); - // Notify popup notifications of the UI change so they show notification - // panels again. - UpdatePopupNotificationsVisibility(); - } - // Check if all prompts are closed - if (!this._hasPrompts()) { - browser.removeAttribute("tabmodalPromptShowing"); - browser.focus(); - } - }, - - /** - * Checks if the prompt box has prompt elements. - * @returns {Boolean} - true if there are prompt elements. - */ - _hasPrompts() { - return !!this._getPromptElements().length; - }, - - /** - * Get list of current prompt elements. - * @param {Number} [aModalType] - Optionally filter by - * Ci.nsIPrompt.MODAL_TYPE_. - * @returns {NodeList} - A list of tabmodalprompt elements. - */ - _getPromptElements(aModalType = null) { - let selector = "tabmodalprompt"; - - if (aModalType != null) { - if (aModalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { - selector += ".tab-prompt"; - } else { - selector += ".content-prompt"; - } - } - return this.browser.parentNode.querySelectorAll(selector); - }, - - /** - * Get a list of all TabModalPrompt objects associated with the prompt box. - * @param {Number} [aModalType] - Optionally filter by - * Ci.nsIPrompt.MODAL_TYPE_. - * @returns {TabModalPrompt[]} - An array of TabModalPrompt objects. - */ - listPrompts(aModalType = null) { - // Get the nodelist, then return the TabModalPrompt instances as an array - let promptMap; - - if (aModalType) { - if (aModalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { - promptMap = this._tabPrompts; - } else { - promptMap = this._contentPrompts; - } - } - - let elements = this._getPromptElements(aModalType); - - if (promptMap) { - return [...elements].map(el => promptMap.get(el)); - } - return [...elements].map( - el => this._contentPrompts.get(el) || this._tabPrompts.get(el) - ); - }, - - onNextPromptShowAllowFocusCheckboxFor(principal) { - this._allowTabFocusByPromptPrincipal = principal; - }, - - get browser() { - let browser = this._weakBrowserRef.get(); - if (!browser) { - throw new Error("Stale promptbox! The associated browser is gone."); - } - return browser; - }, -}; - // Handle window-modal prompts that we want to display with the same style as // tab-modal prompts. var gDialogBox = { diff --git a/browser/base/content/browser.js.globals b/browser/base/content/browser.js.globals index c767bb5beb..7002cd0b5b 100644 --- a/browser/base/content/browser.js.globals +++ b/browser/base/content/browser.js.globals @@ -32,37 +32,19 @@ "delayedStartupPromise", "gBrowserInit", "HandleAppCommandEvent", - "gotoHistoryIndex", - "BrowserForward", - "BrowserBack", - "BrowserHandleBackspace", - "BrowserHandleShiftBackspace", - "BrowserStop", - "BrowserReloadOrDuplicate", - "BrowserReload", + "BrowserCommands", "kSkipCacheFlags", - "BrowserReloadSkipCache", - "BrowserHome", "loadOneOrMoreURIs", "openLocation", - "BrowserOpenTab", "gLastOpenDirectory", - "BrowserOpenFileWindow", - "BrowserCloseTabOrWindow", - "BrowserTryToCloseWindow", "getLoadContext", "readFromClipboard", - "BrowserViewSourceOfDocument", - "BrowserViewSource", - "BrowserPageInfo", "UpdateUrlbarSearchSplitterState", "UpdatePopupNotificationsVisibility", "PageProxyClickHandler", "BrowserOnClick", "getMeOutOfHere", "getDefaultHomePage", - "BrowserFullScreen", - "BrowserReloadWithFlags", "getPEMString", "browserDragAndDrop", "homeButtonObserver", @@ -131,7 +113,6 @@ "PanicButtonNotifier", "SafeBrowsingNotificationBox", "TabDialogBox", - "TabModalPromptBox", "gDialogBox", "ConfirmationHint", "FirefoxViewHandler", @@ -196,7 +177,6 @@ "SubDialog", "SubDialogManager", "TabCrashHandler", - "TabModalPrompt", "TabsSetupFlowManager", "TelemetryEnvironment", "TranslationsParent", diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml index 1dcdd02cd1..a01138c7da 100644 --- a/browser/base/content/browser.xhtml +++ b/browser/base/content/browser.xhtml @@ -38,9 +38,6 @@ both "content" and "skin" packages, which bug 1385444 will unify later. --> <link rel="stylesheet" href="chrome://global/skin/global.css" /> - <link rel="stylesheet" href="chrome://global/content/tabprompts.css" /> - <link rel="stylesheet" href="chrome://global/skin/tabprompts.css" /> - <link rel="stylesheet" href="chrome://browser/content/browser.css" /> <link rel="stylesheet" href="chrome://browser/content/tabbrowser.css" /> <link @@ -98,6 +95,7 @@ <link rel="localization" href="preview/select-translations.ftl"/> <link rel="localization" href="browser/shopping.ftl"/> <link rel="localization" href="preview/shopping.ftl"/> + <link rel="localization" href="preview/sidebar.ftl"/> <link rel="localization" href="preview/profiles.ftl"/> <link rel="localization" href="preview/onboarding.ftl"/> @@ -119,7 +117,7 @@ Services.scriptloader.loadSubScript("chrome://browser/content/browser-development-helpers.js", this); } Services.scriptloader.loadSubScript("chrome://browser/content/browser-pageActions.js", this); - Services.scriptloader.loadSubScript("chrome://browser/content/browser-sidebar.js", this); + Services.scriptloader.loadSubScript("chrome://browser/content/sidebar/browser-sidebar.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/browser-tabsintitlebar.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/browser-unified-extensions.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser.js", this); diff --git a/browser/base/content/contentTheme.js b/browser/base/content/contentTheme.js index 3c46b80bec..2eb339d69b 100644 --- a/browser/base/content/contentTheme.js +++ b/browser/base/content/contentTheme.js @@ -43,8 +43,8 @@ let browserStyle = element.ownerGlobal?.docShell?.chromeEventHandler.style; + element.toggleAttribute("lwt-newtab", !!rgbaChannels); if (!rgbaChannels) { - element.removeAttribute("lwt-newtab"); element.toggleAttribute( "lwt-newtab-brighttext", prefersDarkQuery.matches @@ -55,7 +55,6 @@ return null; } - element.setAttribute("lwt-newtab", "true"); const { r, g, b, a } = rgbaChannels; let darkMode = !_isTextColorDark(r, g, b); element.toggleAttribute("lwt-newtab-brighttext", darkMode); @@ -159,15 +158,10 @@ * @param {Object} event object containing the theme or query update. */ handleEvent(event) { - const root = document.documentElement; - if (event.type == "LightweightTheme:Set") { - let { data } = event.detail; - if (!data) { - data = {}; - } - this._setProperties(root, data); + this._setProperties(event.detail.data || {}); } else if (event.type == "change") { + const root = document.documentElement; // If a lightweight theme doesn't apply, update lwt-newtab-brighttext to // reflect prefers-color-scheme. if (!root.hasAttribute("lwt-newtab")) { @@ -192,22 +186,23 @@ /** * Apply theme data to an element - * @param {Element} root The element where the properties should be applied. * @param {Object} themeData The theme data. */ - _setProperties(elem, themeData) { + _setProperties(themeData) { + const root = document.documentElement; + root.toggleAttribute("lwtheme", themeData.hasTheme); for (let [cssVarName, definition] of inContentVariableMap) { const { lwtProperty, processColor } = definition; let value = themeData[lwtProperty]; if (processColor) { - value = processColor(value, elem); + value = processColor(value, root); } else if (value) { const { r, g, b, a } = value; value = `rgba(${r}, ${g}, ${b}, ${a})`; } - this._setProperty(elem, cssVarName, value); + this._setProperty(root, cssVarName, value); } }, }; diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml index bff8d98b27..91b2483c53 100644 --- a/browser/base/content/main-popupset.inc.xhtml +++ b/browser/base/content/main-popupset.inc.xhtml @@ -100,7 +100,7 @@ oncommand="FullScreen.setAutohide();"/> <menuitem contexttype="fullscreen" data-lazy-l10n-id="full-screen-exit" - oncommand="BrowserFullScreen();"/> + oncommand="BrowserCommands.fullScreen();"/> </menupopup> <!-- bug 415444/582485: event.stopPropagation is here for the cloned version @@ -109,7 +109,7 @@ --> <menupopup id="backForwardMenu" onpopupshowing="return FillHistoryMenu(event.target);" - oncommand="gotoHistoryIndex(event); event.stopPropagation();"/> + oncommand="BrowserCommands.gotoHistoryIndex(event); event.stopPropagation();"/> <tooltip id="aHTMLTooltip" page="true"/> <tooltip id="remoteBrowserTooltip"/> @@ -262,6 +262,9 @@ data-l10n-id="sidebar-menu-synced-tabs" class="sync-ui-item" oncommand="SidebarUI.show('viewTabsSidebar');"/> + <menuitem id="sidebar-switcher-megalist" + data-l10n-id="sidebar-menu-megalist" + oncommand="SidebarUI.show('viewMegalistSidebar');"/> <menuseparator/> <!-- Extension toolbarbuttons go here. --> <menuseparator id="sidebar-extensions-separator"/> @@ -360,7 +363,7 @@ oncommand="FullScreen.setAutohide();"/> <menuitem contexttype="fullscreen" data-lazy-l10n-id="full-screen-exit" - oncommand="BrowserFullScreen();"/> + oncommand="BrowserCommands.fullScreen();"/> </menupopup> <menupopup id="blockedPopupOptions" @@ -412,7 +415,20 @@ <hbox id="ctrlTab-showAll-container" pack="center"/> </panel> - <html:tab-preview id="tabbrowser-tab-preview" hidden="true" /> + <!-- TODO: create lazily? --> + <panel id="tab-preview-panel" + type="arrow" + orient="vertical" + noautofocus="true" + norolluponanchor="true" + rolluponmousewheel="true" + consumeoutsideclicks="false"> + <html:div class="tab-preview-text-container"> + <html:div class="tab-preview-title"></html:div> + <html:div class="tab-preview-uri"></html:div> + </html:div> + <html:div class="tab-preview-thumbnail-container"></html:div> + </panel> <html:template id="pageActionPanelTemplate"> <panel id="pageActionPanel" @@ -624,7 +640,7 @@ oncommand="gUnifiedExtensions.reportExtension(this.parentElement)" /> </menupopup> - <menupopup id="translations-panel-settings-menupopup" + <menupopup id="full-page-translations-panel-settings-menupopup" onpopupshown="FullPageTranslationsPanel.handleSettingsPopupShownEvent()" onpopuphidden="FullPageTranslationsPanel.handleSettingsPopupHiddenEvent()"> <menuitem class="always-offer-translations-menuitem" diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml index fc19910726..a86d01adb2 100644 --- a/browser/base/content/navigator-toolbox.inc.xhtml +++ b/browser/base/content/navigator-toolbox.inc.xhtml @@ -491,13 +491,6 @@ tooltiptext="Ion" onmousedown="switchToTabHavingURI('about:ion', true);" onkeypress="switchToTabHavingURI('about:ion', true);"/> - <toolbarbutton id="whats-new-menu-button" - class="toolbarbutton-1" - delegatesanchor="true" - hidden="true" - badged="true" - onmousedown="PanelUI.showSubView('PanelUI-whatsNew', this, event);" - onkeypress="PanelUI.showSubView('PanelUI-whatsNew', this, event);"/> <toolbarbutton id="PanelUI-menu-button" class="toolbarbutton-1" delegatesanchor="true" @@ -708,7 +701,7 @@ ondragenter="homeButtonObserver.onDragOver(event)" ondrop="homeButtonObserver.onDrop(event)" key="goHome" - onclick="BrowserHome(event);" + onclick="BrowserCommands.home(event);" cui-areatype="toolbar"/> <toolbarbutton id="library-button" class="toolbarbutton-1 chromeclass-toolbar-additional subviewbutton-nav" diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js index 031a32dddf..9769552dce 100644 --- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -1619,7 +1619,7 @@ class nsContextMenu { // Open new "view source" window with the frame's URL. viewFrameSource() { - BrowserViewSourceOfDocument({ + BrowserCommands.viewSourceOfDocument({ browser: this.browser, URL: this.contentData.docLocation, outerWindowID: this.frameOuterWindowID, @@ -1627,7 +1627,7 @@ class nsContextMenu { } viewInfo() { - BrowserPageInfo( + BrowserCommands.pageInfo( this.contentData.docLocation, null, null, @@ -1637,7 +1637,7 @@ class nsContextMenu { } viewImageInfo() { - BrowserPageInfo( + BrowserCommands.pageInfo( this.contentData.docLocation, "mediaTab", this.imageInfo, @@ -1661,7 +1661,7 @@ class nsContextMenu { } viewFrameInfo() { - BrowserPageInfo( + BrowserCommands.pageInfo( this.contentData.docLocation, null, null, @@ -1982,7 +1982,7 @@ class nsContextMenu { // we give up waiting for the filename. function timerCallback() {} timerCallback.prototype = { - notify: function sLA_timer_notify(aTimer) { + notify: function sLA_timer_notify() { channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT); }, }; @@ -2494,23 +2494,6 @@ class nsContextMenu { } /** - * Retrieves an instance of the TranslationsParent actor. - * @returns {TranslationsParent} - The TranslationsParent actor. - * @throws Throws if an instance of the actor cannot be retrieved. - */ - static #getTranslationsActor() { - const actor = - gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor( - "Translations" - ); - - if (!actor) { - throw new Error("Unable to get the TranslationsParent"); - } - return actor; - } - - /** * Determines if Full Page Translations is currently active on this page. * * @returns {boolean} @@ -2518,7 +2501,9 @@ class nsContextMenu { static #isFullPageTranslationsActive() { try { const { requestedTranslationPair } = - this.#getTranslationsActor().languageState; + TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ).languageState; return requestedTranslationPair !== null; } catch { // Failed to retrieve the Full Page Translations actor, do nothing. @@ -2532,7 +2517,16 @@ class nsContextMenu { * @param {Event} event - The triggering event for opening the panel. */ openSelectTranslationsPanel(event) { - SelectTranslationsPanel.open(event, this.#translationsLangPairPromise); + const context = this.contentData.context; + let screenX = context.screenXDevPx / window.devicePixelRatio; + let screenY = context.screenYDevPx / window.devicePixelRatio; + SelectTranslationsPanel.open( + event, + screenX, + screenY, + this.#getTextToTranslate(), + this.#translationsLangPairPromise + ); } /** @@ -2583,6 +2577,17 @@ class nsContextMenu { } /** + * Fetches text for translation, prioritizing selected text over link text. + * + * @returns {string} The text to translate. + */ + #getTextToTranslate() { + return this.isTextSelected + ? this.selectionInfo.fullText.trim() + : this.linkTextStr.trim(); + } + + /** * Displays or hides the translate-selection item in the context menu. */ showTranslateSelectionItem() { @@ -2596,10 +2601,7 @@ class nsContextMenu { "browser.translations.select.enable" ); - // Selected text takes precedence over link text. - const textToTranslate = this.isTextSelected - ? this.selectedText.trim() - : this.linkTextStr.trim(); + const textToTranslate = this.#getTextToTranslate(); translateSelectionItem.hidden = // Only show the item if the feature is enabled. diff --git a/browser/base/content/pageinfo/pageInfo.js b/browser/base/content/pageinfo/pageInfo.js index f3999a7cc5..c186e9572d 100644 --- a/browser/base/content/pageinfo/pageInfo.js +++ b/browser/base/content/pageinfo/pageInfo.js @@ -47,7 +47,7 @@ pageInfoTreeView.prototype = { return this.data[row][column.index] || ""; }, - setCellValue(row, column, value) {}, + setCellValue() {}, setCellText(row, column, value) { this.data[row][column.index] = value; @@ -112,52 +112,52 @@ pageInfoTreeView.prototype = { this.sortcol = treecol.index; }, - getRowProperties(row) { + getRowProperties() { return ""; }, - getCellProperties(row, column) { + getCellProperties() { return ""; }, - getColumnProperties(column) { + getColumnProperties() { return ""; }, - isContainer(index) { + isContainer() { return false; }, - isContainerOpen(index) { + isContainerOpen() { return false; }, - isSeparator(index) { + isSeparator() { return false; }, isSorted() { return this.sortcol > -1; }, - canDrop(index, orientation) { + canDrop() { return false; }, - drop(row, orientation) { + drop() { return false; }, - getParentIndex(index) { + getParentIndex() { return 0; }, - hasNextSibling(index, after) { + hasNextSibling() { return false; }, - getLevel(index) { + getLevel() { return 0; }, - getImageSrc(row, column) {}, + getImageSrc() {}, getCellValue(row, column) { let col = column != null ? column : this.copycol; return row < 0 || col < 0 ? "" : this.data[row][col] || ""; }, - toggleOpenState(index) {}, - cycleHeader(col) {}, + toggleOpenState() {}, + cycleHeader() {}, selectionChanged() {}, - cycleCell(row, column) {}, - isEditable(row, column) { + cycleCell() {}, + isEditable() { return false; }, }; @@ -475,10 +475,10 @@ async function loadTab(args) { function openCacheEntry(key, cb) { var checkCacheListener = { - onCacheEntryCheck(entry) { + onCacheEntryCheck() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, - onCacheEntryAvailable(entry, isNew, status) { + onCacheEntryAvailable(entry) { cb(entry); }, }; @@ -1085,7 +1085,7 @@ let treeController = { return command == "cmd_copy" || command == "cmd_selectAll"; }, - isCommandEnabled(command) { + isCommandEnabled() { return true; // not worth checking for this }, diff --git a/browser/base/content/pageinfo/pageInfo.xhtml b/browser/base/content/pageinfo/pageInfo.xhtml index baa017702f..cca293c534 100644 --- a/browser/base/content/pageinfo/pageInfo.xhtml +++ b/browser/base/content/pageinfo/pageInfo.xhtml @@ -10,6 +10,10 @@ data-l10n-id="page-info-window" data-l10n-attrs="style" windowtype="Browser:page-info" +#ifdef XP_MACOSX + drawtitle="true" + chromemargin="0,0,0,0" +#endif onload="onLoadPageInfo()" align="stretch" screenX="10" screenY="10" diff --git a/browser/base/content/pageinfo/permissions.js b/browser/base/content/pageinfo/permissions.js index 7834e27c98..7803cbafe5 100644 --- a/browser/base/content/pageinfo/permissions.js +++ b/browser/base/content/pageinfo/permissions.js @@ -28,7 +28,7 @@ let gPermissions = SitePermissions.listPermissions() }); var permissionObserver = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic == "perm-changed") { var permission = aSubject.QueryInterface(Ci.nsIPermission); if ( diff --git a/browser/base/content/pageinfo/security.js b/browser/base/content/pageinfo/security.js index 3acd3cc452..9f9fb17bea 100644 --- a/browser/base/content/pageinfo/security.js +++ b/browser/base/content/pageinfo/security.js @@ -399,7 +399,7 @@ function realmHasPasswords(uri) { * * @param host - the domain name to look for in history */ -function previousVisitCount(host, endTimeReference) { +function previousVisitCount(host) { if (!host) { return 0; } diff --git a/browser/base/content/sanitizeDialog.js b/browser/base/content/sanitizeDialog.js index 09a7d927df..1d9ea978b9 100644 --- a/browser/base/content/sanitizeDialog.js +++ b/browser/base/content/sanitizeDialog.js @@ -218,7 +218,7 @@ var gSanitizePromptDialog = { acceptButton.disabled = noneChecked; }, - selectByTimespan() { + async selectByTimespan() { // This method is the onselect handler for the duration dropdown. As a // result it's called a couple of times before onload calls init(). if (!this._inited) { @@ -247,7 +247,7 @@ var gSanitizePromptDialog = { } // make sure the sizes are updated in the new dialog else { - this.updateDataSizesInUI(); + await this.updateDataSizesInUI(); } return; } @@ -267,7 +267,7 @@ var gSanitizePromptDialog = { if (!lazy.USE_OLD_DIALOG) { // We only update data sizes to display on the new dialog - this.updateDataSizesInUI(); + await this.updateDataSizesInUI(); } }, @@ -399,7 +399,7 @@ var gSanitizePromptDialog = { this.cacheSize = lazy.DownloadUtils.convertByteUnits(cacheSize); this._dataSizesUpdated = true; - this.updateDataSizesInUI(); + await this.updateDataSizesInUI(); }, /** @@ -473,7 +473,7 @@ var gSanitizePromptDialog = { /** * Updates data sizes displayed based on new selected timespan */ - updateDataSizesInUI() { + async updateDataSizesInUI() { if (!this._dataSizesUpdated) { return; } @@ -491,6 +491,7 @@ var gSanitizePromptDialog = { let timeSpanSelected = TIMESPAN_SELECTION_MAP[index]; let [amount, unit] = this.siteDataSizes[timeSpanSelected]; + document.l10n.pauseObserving(); document.l10n.setAttributes( this._cookiesAndSiteDataCheckbox, "item-cookies-site-data-with-size", @@ -503,6 +504,18 @@ var gSanitizePromptDialog = { "item-cached-content-with-size", { amount, unit } ); + + // make sure l10n updates are completed + await document.l10n.translateElements([ + this._cookiesAndSiteDataCheckbox, + this._cacheCheckbox, + ]); + + document.l10n.resumeObserving(); + + // the data sizes may have led to wrapping, resize dialog to make sure the buttons + // don't move out of view + await window.resizeDialog(); }, /** diff --git a/browser/base/content/tabbrowser-tab.js b/browser/base/content/tabbrowser-tab.js index ed3d4bb727..807a7d93fd 100644 --- a/browser/base/content/tabbrowser-tab.js +++ b/browser/base/content/tabbrowser-tab.js @@ -259,6 +259,14 @@ return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed; } + /** + * Returns a timestamp which attempts to represent the last time the user saw this tab. + * If the tab has not been active in this session, any lastAccessed is used. We + * differentiate between selected and explicitly visible; a selected tab in a hidden + * window is last seen when that window and tab were last visible. + * We use the application start time as a fallback value when no other suitable value + * is available. + */ get lastSeenActive() { const isForegroundWindow = this.ownerGlobal == @@ -270,8 +278,16 @@ if (this._lastSeenActive) { return this._lastSeenActive; } - // Use the application start time as the fallback value - return Services.startup.getStartupInfo().start.getTime(); + + const appStartTime = Services.startup.getStartupInfo().start.getTime(); + if (!this._lastAccessed || this._lastAccessed >= appStartTime) { + // When the tab was created this session but hasn't been seen by the user, + // default to the application start time. + return appStartTime; + } + // The tab was restored from a previous session but never seen. + // Use the lastAccessed as the best proxy for when the user might have seen it. + return this._lastAccessed; } get _overPlayingIcon() { @@ -457,7 +473,7 @@ } } - on_mouseup(event) { + on_mouseup() { // Make sure that clear-selection is released. // Otherwise selection using Shift key may be broken. gBrowser.unlockClearMultiSelection(); @@ -706,11 +722,11 @@ this.setAttribute("aria-describedby", "tabbrowser-tab-a11y-desc"); } - on_focus(event) { + on_focus() { this.updateA11yDescription(); } - on_AriaFocus(event) { + on_AriaFocus() { this.updateA11yDescription(); } } diff --git a/browser/base/content/tabbrowser-tabs.js b/browser/base/content/tabbrowser-tabs.js index 36b6aeb390..7784e74ed3 100644 --- a/browser/base/content/tabbrowser-tabs.js +++ b/browser/base/content/tabbrowser-tabs.js @@ -34,6 +34,7 @@ this.addEventListener("drop", this); this.addEventListener("dragend", this); this.addEventListener("dragleave", this); + this.addEventListener("mouseleave", this); } init() { @@ -61,6 +62,7 @@ this._hiddenSoundPlayingTabs = new Set(); this._allTabs = null; this._visibleTabs = null; + this._previewPanel = null; var tab = this.allTabs[0]; tab.label = this.emptyTabTitle; @@ -125,12 +127,12 @@ this.configureTooltip = () => { // fall back to original tooltip behavior if pref is not set - this.tooltip = this._showCardPreviews ? null : "tabbrowser-tab-tooltip"; - - // activate new tooltip behavior if pref is set - document - .getElementById("tabbrowser-tab-preview") - .toggleAttribute("hidden", !this._showCardPreviews); + if (this._showCardPreviews) { + this.tooltip = null; + } else { + this.tooltip = "tabbrowser-tab-tooltip"; + this._previewPanel = null; + } }; XPCOMUtils.defineLazyPreferenceGetter( this, @@ -142,7 +144,7 @@ this.configureTooltip(); } - on_TabSelect(event) { + on_TabSelect() { this._handleTabSelect(); } @@ -188,23 +190,23 @@ } on_TabHoverStart(event) { - if (this._showCardPreviews) { - const previewContainer = document.getElementById( - "tabbrowser-tab-preview" + if (!this._showCardPreviews) { + return; + } + if (!this._previewPanel) { + // load the tab preview component + const TabPreviewPanel = ChromeUtils.importESModule( + "chrome://browser/content/tabpreview/tab-preview-panel.mjs" + ).default; + this._previewPanel = new TabPreviewPanel( + document.getElementById("tab-preview-panel") ); - previewContainer.tab = event.target; } + this._previewPanel.activate(event.target); } on_TabHoverEnd(event) { - if (this._showCardPreviews) { - const previewContainer = document.getElementById( - "tabbrowser-tab-preview" - ); - if (previewContainer.tab === event.target) { - previewContainer.tab = null; - } - } + this._previewPanel?.deactivate(event.target); } on_transitionend(event) { @@ -241,7 +243,7 @@ } if (!this._blockDblClick) { - BrowserOpenTab(); + BrowserCommands.openTab(); } event.preventDefault(); @@ -333,7 +335,7 @@ (!RTL_UI && event.clientX > endOfTab) || (RTL_UI && event.clientX < endOfTab) ) { - BrowserOpenTab(); + BrowserCommands.openTab(); } } else { return; @@ -450,6 +452,7 @@ return; } + this._previewPanel?.deactivate(); this.startTabDrag(event, tab); } @@ -1092,6 +1095,10 @@ return children; } + get previewPanel() { + return this._previewPanel; + } + _getVisibleTabs() { if (!this._visibleTabs) { this._visibleTabs = Array.prototype.filter.call( @@ -1205,7 +1212,7 @@ }; } - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "nsPref:changed": // This is has to deal with changes in @@ -1851,6 +1858,9 @@ this._unlockTabSizing(); } break; + case "mouseleave": + this._previewPanel?.deactivate(); + break; default: let methodName = `on_${aEvent.type}`; if (methodName in this) { diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js index 54a801939a..3bca0b6d30 100644 --- a/browser/base/content/tabbrowser.js +++ b/browser/base/content/tabbrowser.js @@ -893,14 +893,6 @@ : ""; }, - getTabModalPromptBox(aBrowser) { - let browser = aBrowser || this.selectedBrowser; - if (!browser.tabModalPromptBox) { - browser.tabModalPromptBox = new TabModalPromptBox(browser); - } - return browser.tabModalPromptBox; - }, - getTabDialogBox(aBrowser) { if (!aBrowser) { throw new Error("aBrowser is required"); @@ -1320,31 +1312,6 @@ this.addToMultiSelectedTabs(oldTab); } - if (oldBrowser != newBrowser && oldBrowser.getInPermitUnload) { - oldBrowser.getInPermitUnload(inPermitUnload => { - if (!inPermitUnload) { - return; - } - // Since the user is switching away from a tab that has - // a beforeunload prompt active, we remove the prompt. - // This prevents confusing user flows like the following: - // 1. User attempts to close Firefox - // 2. User switches tabs (ingoring a beforeunload prompt) - // 3. User returns to tab, presses "Leave page" - let promptBox = this.getTabModalPromptBox(oldBrowser); - let prompts = promptBox.listPrompts(); - // There might not be any prompts here if the tab was closed - // while in an onbeforeunload prompt, which will have - // destroyed aforementioned prompt already, so check there's - // something to remove, first: - if (prompts.length) { - // NB: This code assumes that the beforeunload prompt - // is the top-most prompt on the tab. - prompts[prompts.length - 1].abortPrompt(); - } - }); - } - if (!gMultiProcessBrowser) { this._adjustFocusBeforeTabSwitch(oldTab, newTab); this._adjustFocusAfterTabSwitch(newTab); @@ -1439,19 +1406,6 @@ newBrowser.tabDialogBox.focus(); return; } - if (newBrowser.hasAttribute("tabmodalPromptShowing")) { - // If there's a tabmodal prompt showing, focus it. - let prompts = newBrowser.tabModalPromptBox.listPrompts(); - let prompt = prompts[prompts.length - 1]; - // @tabmodalPromptShowing is also set for other tab modal prompts - // (e.g. the Payment Request dialog) so there may not be a <tabmodalprompt>. - // Bug 1492814 will implement this for the Payment Request dialog. - if (prompt) { - prompt.Dialog.setDefaultFocus(); - return; - } - } - // Focus the location bar if it was previously focused for that tab. // In full screen mode, only bother making the location bar visible // if the tab is a blank one. @@ -2097,18 +2051,6 @@ // doesn't keep the window alive. b.permanentKey = new (Cu.getGlobalForObject(Services).Object)(); - // Ensure that SessionStore has flushed any session history state from the - // content process before we this browser's remoteness. - if (!Services.appinfo.sessionHistoryInParent) { - b.prepareToChangeRemoteness = () => - SessionStore.prepareToChangeRemoteness(b); - b.afterChangeRemoteness = switchId => { - let tab = this.getTabForBrowser(b); - SessionStore.finishTabRemotenessChange(tab, switchId); - return true; - }; - } - const defaultBrowserAttributes = { contextmenu: "contentAreaContextMenu", message: "true", @@ -2682,8 +2624,6 @@ animate, userContextId, openerTab, - createLazyBrowser, - skipAnimation, pinned, noInitialLabel, skipBackgroundNotify, @@ -2849,8 +2789,6 @@ uriString, userContextId, openerTab, - createLazyBrowser, - skipAnimation, pinned, noInitialLabel, skipBackgroundNotify, @@ -3367,7 +3305,7 @@ Services.telemetry.setEventRecordingEnabled("close_tab_warning", true); let closeTabEnumKey = Object.entries(this.closingTabsEnum) - .find(([k, v]) => v == aCloseTabs)?.[0] + .find(([, v]) => v == aCloseTabs)?.[0] ?.toLowerCase() || "some"; let warnCheckbox = warnOnClose.value ? "checked" : "unchecked"; @@ -3674,6 +3612,8 @@ tabs, { animate, + // See bug 1883051 + // eslint-disable-next-line no-unused-vars suppressWarnAboutClosingWindow, skipPermitUnload, skipRemoves, @@ -4406,7 +4346,7 @@ } // Do nothing if (event.button == 1) { - BrowserOpenTab({ event }); + BrowserCommands.openTab({ event }); // Stop the propagation of the click event, to prevent the event from being // handled more than once. // E.g. see https://bugzilla.mozilla.org/show_bug.cgi?id=1657992#c4 @@ -5849,7 +5789,7 @@ } }, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "contextual-identity-updated": { let identity = aSubject.wrappedJSObject; @@ -6105,12 +6045,7 @@ ); if (permission != Services.perms.ALLOW_ACTION) { // Tell the prompt box we want to show the user a checkbox: - let tabPrompt = Services.prefs.getBoolPref( - "prompts.contentPromptSubDialog" - ) - ? this.getTabDialogBox(tabForEvent.linkedBrowser) - : this.getTabModalPromptBox(tabForEvent.linkedBrowser); - + let tabPrompt = this.getTabDialogBox(tabForEvent.linkedBrowser); tabPrompt.onNextPromptShowAllowFocusCheckboxFor( promptPrincipal ); @@ -6352,7 +6287,7 @@ let oldUserTypedValue = browser.userTypedValue; let hadStartedLoad = browser.didStartLoadSinceLastUserTyping(); - let didChange = didChangeEvent => { + let didChange = () => { browser.userTypedValue = oldUserTypedValue; if (hadStartedLoad) { browser.urlbarChangeTracker.startedLoad(); @@ -7461,7 +7396,7 @@ var TabBarVisibility = { toolbar.collapsed = collapse; let navbar = document.getElementById("nav-bar"); - navbar.setAttribute("tabs-hidden", collapse); + navbar.toggleAttribute("tabs-hidden", collapse); document.getElementById("menu_closeWindow").hidden = collapse; document.l10n.setAttributes( @@ -7785,7 +7720,7 @@ var TabContextMenu = { } }, - closeContextTabs(event) { + closeContextTabs() { if (this.contextTab.multiselected) { gBrowser.removeMultiSelectedTabs(); } else { diff --git a/browser/base/content/test/about/browser_aboutCertError.js b/browser/base/content/test/about/browser_aboutCertError.js index 9af82b807f..5939b026bd 100644 --- a/browser/base/content/test/about/browser_aboutCertError.js +++ b/browser/base/content/test/about/browser_aboutCertError.js @@ -121,7 +121,7 @@ add_task(async function checkReturnToPreviousPage() { "pageshow", true ); - await SpecialPowers.spawn(bc, [useFrame], async function (subFrame) { + await SpecialPowers.spawn(bc, [useFrame], async function () { let returnButton = content.document.getElementById("returnButton"); returnButton.click(); }); @@ -544,7 +544,7 @@ add_task(async function checkViewSource() { certOverrideService.clearValidityOverride("expired.example.com", -1, {}); loaded = BrowserTestUtils.waitForErrorPage(browser); - BrowserReloadSkipCache(); + BrowserCommands.reloadSkipCache(); await loaded; BrowserTestUtils.removeTab(gBrowser.selectedTab); diff --git a/browser/base/content/test/alerts/browser_notification_open_settings.js b/browser/base/content/test/alerts/browser_notification_open_settings.js index ed51cd782b..e7f1c28251 100644 --- a/browser/base/content/test/alerts/browser_notification_open_settings.js +++ b/browser/base/content/test/alerts/browser_notification_open_settings.js @@ -14,7 +14,7 @@ add_task(async function test_settingsOpen_observer() { gBrowser, url: "about:robots", }, - async function dummyTabTask(aBrowser) { + async function dummyTabTask() { // Ensure preferences is loaded before removing the tab. let syncPaneLoadedPromise = TestUtils.topicObserved( "sync-pane-loaded", diff --git a/browser/base/content/test/alerts/head.js b/browser/base/content/test/alerts/head.js index 4be18f6c41..eaf3a2bb74 100644 --- a/browser/base/content/test/alerts/head.js +++ b/browser/base/content/test/alerts/head.js @@ -20,7 +20,7 @@ async function addNotificationPermission(originString) { */ function promiseWindowClosed(window) { return new Promise(function (resolve) { - Services.ww.registerNotification(function observer(subject, topic, data) { + Services.ww.registerNotification(function observer(subject, topic) { if (topic == "domwindowclosed" && subject == window) { Services.ww.unregisterNotification(observer); resolve(); diff --git a/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js index 6389338a6f..b65a419884 100644 --- a/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js +++ b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js @@ -117,7 +117,7 @@ add_task(async function testCaptivePortalAdvancedPanel() { await BrowserTestUtils.waitForLocationChange(gBrowser, BAD_CERT_PAGE); info("(waitForLocationChange resolved)"); })(); - await SpecialPowers.spawn(browser, [BAD_CERT_PAGE], async expectedURL => { + await SpecialPowers.spawn(browser, [BAD_CERT_PAGE], async () => { const doc = content.document; let advancedButton = doc.getElementById("advancedButton"); await ContentTaskUtils.waitForCondition( diff --git a/browser/base/content/test/contextMenu/browser.toml b/browser/base/content/test/contextMenu/browser.toml index 3eb6a1d606..660f6a955b 100644 --- a/browser/base/content/test/contextMenu/browser.toml +++ b/browser/base/content/test/contextMenu/browser.toml @@ -8,7 +8,6 @@ support-files = [ "subtst_contextmenu_xul.xhtml", "ctxmenu-image.png", "../general/head.js", - "../general/video.ogg", "../general/audio.ogg", "../../../../../toolkit/components/pdfjs/test/file_pdfjs_test.pdf", "contextmenu_common.js", @@ -19,6 +18,7 @@ support-files = [ ["browser_bug1798178.js"] ["browser_contextmenu.js"] +support-files = [ "../general/video.webm" ] tags = "fullscreen" skip-if = [ "os == 'linux'", @@ -86,6 +86,8 @@ skip-if = ["os == 'linux' && socketprocess_networking"] ["browser_strip_on_share_link.js"] +["browser_strip_on_share_nested_link.js"] + ["browser_utilityOverlay.js"] https_first_disabled = true skip-if = ["os == 'linux' && socketprocess_networking"] diff --git a/browser/base/content/test/contextMenu/browser_contextmenu.js b/browser/base/content/test/contextMenu/browser_contextmenu.js index ebeb4bdb04..00da3113c6 100644 --- a/browser/base/content/test/contextMenu/browser_contextmenu.js +++ b/browser/base/content/test/contextMenu/browser_contextmenu.js @@ -137,7 +137,7 @@ add_task(async function test_setup_html() { audio.loop = true; audio.src = "audio.ogg"; video.loop = true; - video.src = "video.ogg"; + video.src = "video.webm"; let awaitPause = ContentTaskUtils.waitForEvent(audio, "pause"); await ContentTaskUtils.waitForCondition( diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_badiframe.js b/browser/base/content/test/contextMenu/browser_contextmenu_badiframe.js index 991a55af70..57d9808f5d 100644 --- a/browser/base/content/test/contextMenu/browser_contextmenu_badiframe.js +++ b/browser/base/content/test/contextMenu/browser_contextmenu_badiframe.js @@ -30,7 +30,7 @@ async function openTestPage() { let pageAndIframesLoaded = BrowserTestUtils.browserLoaded( browser, true /* includeSubFrames */, - url => { + () => { expectedLoads--; return !expectedLoads; }, diff --git a/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js b/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js index 062fbeac08..7e6b71e8e4 100644 --- a/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js +++ b/browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js @@ -64,7 +64,7 @@ add_task(async function test_save_link_blocked_by_extension() { setTimeout(resolve, 0); }; - MockFilePicker.showCallback = function (fp) { + MockFilePicker.showCallback = function () { ok(false, "filepicker should never been shown"); setTimeout(resolve, 0); return Ci.nsIFilePicker.returnCancel; diff --git a/browser/base/content/test/contextMenu/browser_strip_on_share_nested_link.js b/browser/base/content/test/contextMenu/browser_strip_on_share_nested_link.js new file mode 100644 index 0000000000..d11649e648 --- /dev/null +++ b/browser/base/content/test/contextMenu/browser_strip_on_share_nested_link.js @@ -0,0 +1,162 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let listService; + +const TEST_URL = + "https://example.com/browser/browser/base/content/test/general/dummy_page.html"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.query_stripping.strip_list", "stripParam"]], + }); + + // Get the list service so we can wait for it to be fully initialized before running tests. + listService = Cc["@mozilla.org/query-stripping-list-service;1"].getService( + Ci.nsIURLQueryStrippingListService + ); + + await listService.testWaitForInit(); +}); + +/* +Tests the strip-on-share feature for in-content links with nested urls +*/ + +// Testing nested stripping with global params +add_task(async function testNestedStrippingGlobalParam() { + let validUrl = + "https://www.example.com/?test=https%3A%2F%2Fwww.example.net%2F%3Futm_ad%3D1234"; + let shortenedUrl = + "https://www.example.com/?test=https%3A%2F%2Fwww.example.net%2F"; + await testStripOnShare({ + originalURI: validUrl, + strippedURI: shortenedUrl, + }); +}); + +// Testing nested stripping with site specific params +add_task(async function testNestedStrippingSiteSpecific() { + let validUrl = + "https://www.example.com/?test=https%3A%2F%2Fwww.example.net%2F%3Ftest_3%3D1234"; + let shortenedUrl = + "https://www.example.com/?test=https%3A%2F%2Fwww.example.net%2F"; + await testStripOnShare({ + originalURI: validUrl, + strippedURI: shortenedUrl, + }); +}); + +// Testing nested stripping with incorrect site specific params +add_task(async function testNoStrippedNestedParam() { + let validUrl = + "https://www.example.com/?test=https%3A%2F%2Fwww.example.com%2F%3Ftest_3%3D1234"; + let shortenedUrl = + "https://www.example.com/?test=https%3A%2F%2Fwww.example.com%2F%3Ftest_3%3D1234"; + await testStripOnShare({ + originalURI: validUrl, + strippedURI: shortenedUrl, + }); +}); + +// Testing order of stripping for nested stripping +add_task(async function testOrderOfStripping() { + let validUrl = + "https://www.example.com/?test_1=https%3A%2F%2Fwww.example.net%2F%3Ftest_3%3D1234"; + let shortenedUrl = "https://www.example.com/"; + await testStripOnShare({ + originalURI: validUrl, + strippedURI: shortenedUrl, + }); +}); + +// Testing correct scoping of site specific params for nested stripping +add_task(async function testMultipleQueryParamsWithNestedStripping() { + let validUrl = + "https://www.example.com/?test_3=1234&test=https%3A%2F%2Fwww.example.net%2F%3Ftest_3%3D1234"; + let shortenedUrl = + "https://www.example.com/?test_3=1234&test=https%3A%2F%2Fwww.example.net%2F"; + await testStripOnShare({ + originalURI: validUrl, + strippedURI: shortenedUrl, + }); +}); + +// Testing functionality with no https pages +add_task(async function testNonHTTPsPages() { + let validUrl = "https://www.example.com/?test_2=1234&test=about%3A%3Aconfig"; + let shortenedUrl = "https://www.example.com/?test=about%3A%3Aconfig"; + await testStripOnShare({ + originalURI: validUrl, + strippedURI: shortenedUrl, + }); +}); + +/** + * Opens a new tab, opens the context menu and checks that the strip-on-share menu item is visible. + * Checks that the stripped version of the url is copied to the clipboard. + * + * @param {string} originalURI - The orginal url before the stripping occurs + * @param {string} strippedURI - The expected url after stripping occurs + */ +async function testStripOnShare({ originalURI, strippedURI }) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.query_stripping.strip_on_share.enabled", true], + ["privacy.query_stripping.strip_on_share.enableTestMode", true], + ], + }); + + let testJson = { + global: { + queryParams: ["utm_ad"], + topLevelSites: ["*"], + }, + example: { + queryParams: ["test_2", "test_1"], + topLevelSites: ["www.example.com"], + }, + exampleNet: { + queryParams: ["test_3", "test_4"], + topLevelSites: ["www.example.net"], + }, + }; + + await listService.testSetList(testJson); + + await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) { + // Prepare a link + await SpecialPowers.spawn(browser, [originalURI], function (startingURI) { + let link = content.document.createElement("a"); + link.href = startingURI; + link.textContent = "link with query param"; + link.id = "link"; + content.document.body.appendChild(link); + }); + let contextMenu = document.getElementById("contentAreaContextMenu"); + // Open the context menu + let awaitPopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#link", + { type: "contextmenu", button: 2 }, + browser + ); + await awaitPopupShown; + let awaitPopupHidden = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + let stripOnShare = contextMenu.querySelector("#context-stripOnShareLink"); + Assert.ok(BrowserTestUtils.isVisible(stripOnShare), "Menu item is visible"); + // Make sure the stripped link will be copied to the clipboard + await SimpleTest.promiseClipboardChange(strippedURI, () => { + contextMenu.activateItem(stripOnShare); + }); + await awaitPopupHidden; + }); +} diff --git a/browser/base/content/test/contextMenu/contextmenu_common.js b/browser/base/content/test/contextMenu/contextmenu_common.js index ac61aa2a3a..2c9a1967f6 100644 --- a/browser/base/content/test/contextMenu/contextmenu_common.js +++ b/browser/base/content/test/contextMenu/contextmenu_common.js @@ -39,7 +39,7 @@ function closeContextMenu() { contextMenu.hidePopup(); } -function getVisibleMenuItems(aMenu, aData) { +function getVisibleMenuItems(aMenu) { var items = []; var accessKeys = {}; for (var i = 0; i < aMenu.children.length; i++) { @@ -65,7 +65,7 @@ function getVisibleMenuItems(aMenu, aData) { var label = item.getAttribute("label"); ok(label.length, "menuitem " + item.id + " has a label"); if (isGenerated) { - is(key, "", "Generated items shouldn't have an access key"); + is(key, null, "Generated items shouldn't have an access key"); items.push("*" + label); } else if ( item.id.indexOf("spell-check-dictionary-") != 0 && diff --git a/browser/base/content/test/contextMenu/subtst_contextmenu.html b/browser/base/content/test/contextMenu/subtst_contextmenu.html index 2c263fbce4..2facd9fecc 100644 --- a/browser/base/content/test/contextMenu/subtst_contextmenu.html +++ b/browser/base/content/test/contextMenu/subtst_contextmenu.html @@ -26,14 +26,14 @@ document.getElementById("shadow-host-in-link").attachShadow({ mode: "closed" }). <image id="test-svg-image" href="ctxmenu-image.png"/> </svg> <canvas id="test-canvas" width="100" height="100" style="background-color: blue"></canvas> -<video controls id="test-video-ok" src="video.ogg" width="100" height="100" style="background-color: green"></video> +<video controls id="test-video-ok" src="video.webm" width="100" height="100" style="background-color: green"></video> <video id="test-audio-in-video" src="audio.ogg" width="100" height="100" style="background-color: red"></video> <video controls id="test-video-bad" src="bogus.duh" width="100" height="100" style="background-color: orange"></video> <video controls id="test-video-bad2" width="100" height="100" style="background-color: yellow"> <source src="bogus.duh" type="video/durrrr;"> </video> <iframe id="test-iframe" width="98" height="98" style="border: 1px solid black"></iframe> -<iframe id="test-video-in-iframe" src="video.ogg" width="98" height="98" style="border: 1px solid black"></iframe> +<iframe id="test-video-in-iframe" src="video.webm" width="98" height="98" style="border: 1px solid black"></iframe> <iframe id="test-audio-in-iframe" src="audio.ogg" width="98" height="98" style="border: 1px solid black"></iframe> <iframe id="test-image-in-iframe" src="ctxmenu-image.png" width="98" height="98" style="border: 1px solid black"></iframe> <iframe id="test-pdf-viewer-in-frame" src="file_pdfjs_test.pdf" width="100" height="100" style="border: 1px solid black"></iframe> diff --git a/browser/base/content/test/contextMenu/subtst_contextmenu_webext.html b/browser/base/content/test/contextMenu/subtst_contextmenu_webext.html index ac3b5415dd..be45c2ddd0 100644 --- a/browser/base/content/test/contextMenu/subtst_contextmenu_webext.html +++ b/browser/base/content/test/contextMenu/subtst_contextmenu_webext.html @@ -7,6 +7,6 @@ <body> Browser context menu subtest. <a href="moz-extension://foo-bar/tab.html" id="link">Link to an extension resource</a> - <video src="moz-extension://foo-bar/video.ogg" id="video"></video> + <video src="moz-extension://foo-bar/video.webm" id="video"></video> </body> </html> diff --git a/browser/base/content/test/favicons/browser_favicon_change_not_in_document.js b/browser/base/content/test/favicons/browser_favicon_change_not_in_document.js index b8215dcc3e..85240aaa95 100644 --- a/browser/base/content/test/favicons/browser_favicon_change_not_in_document.js +++ b/browser/base/content/test/favicons/browser_favicon_change_not_in_document.js @@ -36,8 +36,8 @@ add_task(async function () { )); let domLinkAddedFired = 0; let domLinkChangedFired = 0; - const linkAddedHandler = event => domLinkAddedFired++; - const linkChangedhandler = event => domLinkChangedFired++; + const linkAddedHandler = () => domLinkAddedFired++; + const linkChangedhandler = () => domLinkChangedFired++; BrowserTestUtils.addContentEventListener( gBrowser.selectedBrowser, "DOMLinkAdded", @@ -80,8 +80,8 @@ add_task(async function () { let domLinkAddedFired = 0; let domLinkChangedFired = 0; - const linkAddedHandler = event => domLinkAddedFired++; - const linkChangedhandler = event => domLinkChangedFired++; + const linkAddedHandler = () => domLinkAddedFired++; + const linkChangedhandler = () => domLinkChangedFired++; BrowserTestUtils.addContentEventListener( browser, "DOMLinkAdded", diff --git a/browser/base/content/test/favicons/browser_favicon_load.js b/browser/base/content/test/favicons/browser_favicon_load.js index 10c2b8f24e..7b78ae494f 100644 --- a/browser/base/content/test/favicons/browser_favicon_load.js +++ b/browser/base/content/test/favicons/browser_favicon_load.js @@ -50,7 +50,7 @@ function FaviconObserver(aPageURI, aFaviconURL, aTailingEnabled) { } FaviconObserver.prototype = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { // Make sure that the topic is 'http-on-modify-request'. if (aTopic === "http-on-modify-request") { let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); diff --git a/browser/base/content/test/favicons/browser_favicon_nostore.js b/browser/base/content/test/favicons/browser_favicon_nostore.js index 3fec666bbe..c12c7a87cd 100644 --- a/browser/base/content/test/favicons/browser_favicon_nostore.js +++ b/browser/base/content/test/favicons/browser_favicon_nostore.js @@ -140,20 +140,17 @@ add_task(async function root_icon_stored() { response.write("<html>A page without icon</html>"); }); - let noStorePromise = TestUtils.topicObserved( - "http-on-stop-request", - (s, t, d) => { - let chan = s.QueryInterface(Ci.nsIHttpChannel); - return chan?.URI.spec == "http://www.nostore.com/favicon.ico"; - } - ).then(([chan]) => chan.isNoStoreResponse()); + let noStorePromise = TestUtils.topicObserved("http-on-stop-request", s => { + let chan = s.QueryInterface(Ci.nsIHttpChannel); + return chan?.URI.spec == "http://www.nostore.com/favicon.ico"; + }).then(([chan]) => chan.isNoStoreResponse()); await BrowserTestUtils.withNewTab( { gBrowser, url: "http://www.nostore.com/page", }, - async function (browser) { + async function () { await TestUtils.waitForCondition(async () => { let uri = await new Promise(resolve => PlacesUtils.favicons.getFaviconURLForPage( diff --git a/browser/base/content/test/favicons/browser_favicon_referer.js b/browser/base/content/test/favicons/browser_favicon_referer.js index ed332e7413..9fee9771b0 100644 --- a/browser/base/content/test/favicons/browser_favicon_referer.js +++ b/browser/base/content/test/favicons/browser_favicon_referer.js @@ -14,7 +14,7 @@ add_task(async function test_check_referrer_for_discovered_favicon() { async browser => { let referrerPromise = TestUtils.topicObserved( "http-on-modify-request", - (s, t, d) => { + s => { let chan = s.QueryInterface(Ci.nsIHttpChannel); return chan.URI.spec == "http://mochi.test:8888/favicon.ico"; } @@ -42,7 +42,7 @@ add_task( async browser => { let referrerPromise = TestUtils.topicObserved( "http-on-modify-request", - (s, t, d) => { + s => { let chan = s.QueryInterface(Ci.nsIHttpChannel); return chan.URI.spec == `${FOLDER}file_favicon.png`; } diff --git a/browser/base/content/test/favicons/browser_missing_favicon.js b/browser/base/content/test/favicons/browser_missing_favicon.js index f619425909..fd60d362b4 100644 --- a/browser/base/content/test/favicons/browser_missing_favicon.js +++ b/browser/base/content/test/favicons/browser_missing_favicon.js @@ -28,7 +28,7 @@ add_task(async () => { is(browser.mIconURL, null, "Should have blanked the icon."); is( gBrowser.getTabForBrowser(browser).getAttribute("image"), - "", + null, "Should have blanked the tab icon." ); } diff --git a/browser/base/content/test/forms/browser_selectpopup.js b/browser/base/content/test/forms/browser_selectpopup.js index abcdee486f..72112974c2 100644 --- a/browser/base/content/test/forms/browser_selectpopup.js +++ b/browser/base/content/test/forms/browser_selectpopup.js @@ -186,7 +186,7 @@ async function doSelectTests(contentType, content) { ); // Backspace should not go back - let handleKeyPress = function (event) { + let handleKeyPress = function () { ok(false, "Should not get keypress event"); }; window.addEventListener("keypress", handleKeyPress); @@ -708,7 +708,7 @@ add_task(async function test_mousemove_correcttarget() { window, "sizemodechange" ); - BrowserFullScreen(); + BrowserCommands.fullScreen(); await sizeModeChanged; await popupHiddenPromise; } diff --git a/browser/base/content/test/forms/browser_selectpopup_colors.js b/browser/base/content/test/forms/browser_selectpopup_colors.js index 63cece0ce5..f4b3e8a516 100644 --- a/browser/base/content/test/forms/browser_selectpopup_colors.js +++ b/browser/base/content/test/forms/browser_selectpopup_colors.js @@ -255,7 +255,7 @@ function rgbaToString(parsedColor) { return `rgba(${r}, ${g}, ${b}, ${a})`; } -function testOptionColors(test, index, item, menulist) { +function testOptionColors(test, index, item) { // The label contains a JSON string of the expected colors for // `color` and `background-color`. let expected = JSON.parse(item.label); diff --git a/browser/base/content/test/forms/browser_selectpopup_dir.js b/browser/base/content/test/forms/browser_selectpopup_dir.js index aaf4a61fc2..a0ad90d909 100644 --- a/browser/base/content/test/forms/browser_selectpopup_dir.js +++ b/browser/base/content/test/forms/browser_selectpopup_dir.js @@ -13,7 +13,7 @@ add_task(async function () { gBrowser, url, }, - async function (browser) { + async function () { let popup = await openSelectPopup("click"); is(popup.style.direction, "rtl", "Should be the right dir"); } diff --git a/browser/base/content/test/forms/browser_selectpopup_large.js b/browser/base/content/test/forms/browser_selectpopup_large.js index 722e0d9588..40f6d1b160 100644 --- a/browser/base/content/test/forms/browser_selectpopup_large.js +++ b/browser/base/content/test/forms/browser_selectpopup_large.js @@ -297,7 +297,7 @@ add_task(async function test_large_popup_in_small_window() { newWin, "resize", false, - e => { + () => { info(`Got resize event (innerHeight: ${newWin.innerHeight})`); return newWin.innerHeight <= 450; } diff --git a/browser/base/content/test/forms/browser_selectpopup_minFontSize.js b/browser/base/content/test/forms/browser_selectpopup_minFontSize.js index d240c2d2d0..522ed1ffcf 100644 --- a/browser/base/content/test/forms/browser_selectpopup_minFontSize.js +++ b/browser/base/content/test/forms/browser_selectpopup_minFontSize.js @@ -20,7 +20,7 @@ add_task(async function () { gBrowser, url, }, - async function (browser) { + async function () { let popup = await openSelectPopup("click"); let menuitems = popup.querySelectorAll("menuitem"); is( diff --git a/browser/base/content/test/forms/browser_selectpopup_text_transform.js b/browser/base/content/test/forms/browser_selectpopup_text_transform.js index 671f39e2a6..04da532ddc 100644 --- a/browser/base/content/test/forms/browser_selectpopup_text_transform.js +++ b/browser/base/content/test/forms/browser_selectpopup_text_transform.js @@ -16,7 +16,7 @@ add_task(async function () { gBrowser, url, }, - async function (browser) { + async function () { let popup = await openSelectPopup("click"); let menuitems = popup.querySelectorAll("menuitem"); is(menuitems[0].textContent, "abc", "Option text should be lowercase"); diff --git a/browser/base/content/test/forms/browser_selectpopup_user_input.js b/browser/base/content/test/forms/browser_selectpopup_user_input.js index b3cdeaf7e6..028ceadf9a 100644 --- a/browser/base/content/test/forms/browser_selectpopup_user_input.js +++ b/browser/base/content/test/forms/browser_selectpopup_user_input.js @@ -71,7 +71,7 @@ async function testHandlingUserInputOnChange(aTriggerFn) { // This test checks if the change/click event is considered as user input event. add_task(async function test_handling_user_input_key() { - return testHandlingUserInputOnChange(async function (popup) { + return testHandlingUserInputOnChange(async function () { EventUtils.synthesizeKey("KEY_ArrowDown"); await hideSelectPopup(); }); diff --git a/browser/base/content/test/forms/browser_selectpopup_width.js b/browser/base/content/test/forms/browser_selectpopup_width.js index d8f748fb18..0df0fd24ee 100644 --- a/browser/base/content/test/forms/browser_selectpopup_width.js +++ b/browser/base/content/test/forms/browser_selectpopup_width.js @@ -19,7 +19,7 @@ add_task(async function () { gBrowser, url, }, - async function (browser) { + async function () { let popup = await openSelectPopup("click"); let arrowSB = popup.shadowRoot.querySelector(".menupopup-arrowscrollbox"); is( diff --git a/browser/base/content/test/forms/browser_selectpopup_xhtml.js b/browser/base/content/test/forms/browser_selectpopup_xhtml.js index 091649be89..27597eb5ac 100644 --- a/browser/base/content/test/forms/browser_selectpopup_xhtml.js +++ b/browser/base/content/test/forms/browser_selectpopup_xhtml.js @@ -21,7 +21,7 @@ add_task(async function () { gBrowser, url, }, - async function (browser) { + async function () { let popup = await openSelectPopup("click"); let menuitems = popup.querySelectorAll("menuitem"); is(menuitems.length, 2, "Should've properly detected two menu items"); diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_context_menu.js b/browser/base/content/test/fullscreen/browser_fullscreen_context_menu.js index 9d9891acd2..3bca1a205d 100644 --- a/browser/base/content/test/fullscreen/browser_fullscreen_context_menu.js +++ b/browser/base/content/test/fullscreen/browser_fullscreen_context_menu.js @@ -51,7 +51,7 @@ async function testContextMenu() { window, "sizemodechange", false, - e => window.fullScreen + () => window.fullScreen ), BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden"), ]); @@ -96,7 +96,7 @@ async function testContextMenu() { window, "sizemodechange", false, - e => !window.fullScreen + () => !window.fullScreen ), BrowserTestUtils.waitForPopupEvent(contextMenu2, "hidden"), ]); diff --git a/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js b/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js index 5dd71e1a92..6e471e8124 100644 --- a/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js +++ b/browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js @@ -74,7 +74,7 @@ async function testWindowElementFocus(isPopup) { false, async () => { info("Calling element.focus() on popup"); - await ContentTask.spawn(tab.linkedBrowser, {}, async args => { + await ContentTask.spawn(tab.linkedBrowser, {}, async () => { await content.wrappedJSObject.sendMessage( content.wrappedJSObject.openedWindow, "elementfocus" diff --git a/browser/base/content/test/fullscreen/head.js b/browser/base/content/test/fullscreen/head.js index 4d5543461e..0d56c5a7c9 100644 --- a/browser/base/content/test/fullscreen/head.js +++ b/browser/base/content/test/fullscreen/head.js @@ -5,7 +5,7 @@ function waitForFullScreenState(browser, state, actionAfterFSEvent) { return new Promise(resolve => { let eventReceived = false; - let observe = (subject, topic, data) => { + let observe = () => { if (!eventReceived) { return; } diff --git a/browser/base/content/test/general/browser.toml b/browser/base/content/test/general/browser.toml index 6928ba2d4b..31d519d550 100644 --- a/browser/base/content/test/general/browser.toml +++ b/browser/base/content/test/general/browser.toml @@ -37,10 +37,6 @@ support-files = [ "title_test.svg", "unknownContentType_file.pif", "unknownContentType_file.pif^headers^", - "video.ogg", - "web_video.html", - "web_video1.ogv", - "web_video1.ogv^headers^", "!/image/test/mochitest/blue.png", "!/toolkit/content/tests/browser/common/mockTransfer.js", ] @@ -211,6 +207,7 @@ support-files = [ "dummy.ics", "dummy.ics^headers^", "redirect_download.sjs", + "video.webm", ] # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD. @@ -435,9 +432,19 @@ skip-if = [ "os == 'win' && debug", "os =='linux'", #Bug 1212419 ] +support-files = [ + "web_video.html", + "web_video1.webm", + "web_video1.webm^headers^", +] # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD. ["browser_save_video_frame.js"] +support-files = [ + "web_video.html", + "web_video1.webm", + "web_video1.webm^headers^", +] # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD. ["browser_selectTabAtIndex.js"] diff --git a/browser/base/content/test/general/browser_accesskeys.js b/browser/base/content/test/general/browser_accesskeys.js index 0809553404..965da8d9df 100644 --- a/browser/base/content/test/general/browser_accesskeys.js +++ b/browser/base/content/test/general/browser_accesskeys.js @@ -122,7 +122,7 @@ add_task(async function () { function performAccessKey(browser, key) { return new Promise(resolve => { let removeFocus, removeKeyDown, removeKeyUp; - function callback(eventName, result) { + function callback() { removeFocus(); removeKeyUp(); removeKeyDown(); @@ -190,7 +190,7 @@ function performAccessKey(browser, key) { } // This version is used when a chrome element is expected to be found for an accesskey. -async function performAccessKeyForChrome(key, inChild) { +async function performAccessKeyForChrome(key) { let waitFocusChangePromise = BrowserTestUtils.waitForEvent( document, "focus", diff --git a/browser/base/content/test/general/browser_alltabslistener.js b/browser/base/content/test/general/browser_alltabslistener.js index c7829d16fe..fc950d6ce5 100644 --- a/browser/base/content/test/general/browser_alltabslistener.js +++ b/browser/base/content/test/general/browser_alltabslistener.js @@ -7,16 +7,9 @@ function getOriginalURL(request) { } var gFrontProgressListener = { - onProgressChange( - aWebProgress, - aRequest, - aCurSelfProgress, - aMaxSelfProgress, - aCurTotalProgress, - aMaxTotalProgress - ) {}, + onProgressChange() {}, - onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aWebProgress, aRequest, aStateFlags) { var url = getOriginalURL(aRequest); if (url == "about:blank") { return; @@ -28,7 +21,7 @@ var gFrontProgressListener = { assertCorrectBrowserAndEventOrderForFront(state); }, - onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) { + onLocationChange(aWebProgress, aRequest, aLocationURI) { var url = getOriginalURL(aRequest); if (url == "about:blank") { return; @@ -64,7 +57,7 @@ function assertCorrectBrowserAndEventOrderForFront(aEventName) { } var gAllProgressListener = { - onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags) { var url = getOriginalURL(aRequest); if (url == "about:blank") { // ignore initial about blank diff --git a/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js index 8eb07a863a..81ed5a1040 100644 --- a/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js +++ b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js @@ -57,7 +57,7 @@ add_task(async function closeWindowWithMultipleTabsIncludingOneBeforeUnload() { ); let windowClosedPromise = BrowserTestUtils.domWindowClosed(newWin); expectingDialog = true; - newWin.BrowserTryToCloseWindow(); + newWin.BrowserCommands.tryToCloseWindow(); await windowClosedPromise; ok(!expectingDialog, "There should have been a dialog."); ok(newWin.closed, "Window should be closed."); @@ -71,7 +71,7 @@ add_task(async function closeWindoWithSingleTabTwice() { let windowClosedPromise = BrowserTestUtils.domWindowClosed(newWin); expectingDialog = true; wantToClose = false; - let firstDialogShownPromise = new Promise((resolve, reject) => { + let firstDialogShownPromise = new Promise(resolve => { resolveDialogPromise = resolve; }); firstTab.closeButton.click(); diff --git a/browser/base/content/test/general/browser_bug356571.js b/browser/base/content/test/general/browser_bug356571.js index aa3569c93d..69b45e040d 100644 --- a/browser/base/content/test/general/browser_bug356571.js +++ b/browser/base/content/test/general/browser_bug356571.js @@ -45,7 +45,7 @@ const kURIs = ["bad://www.mozilla.org/", kDummyPage, kDummyPage]; var gProgressListener = { _runCount: 0, - onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags) { if ((aStateFlags & kCompleteState) == kCompleteState) { if (++this._runCount != kURIs.length) { return; diff --git a/browser/base/content/test/general/browser_bug417483.js b/browser/base/content/test/general/browser_bug417483.js index 68e2e99511..28da91eea1 100644 --- a/browser/base/content/test/general/browser_bug417483.js +++ b/browser/base/content/test/general/browser_bug417483.js @@ -8,7 +8,7 @@ add_task(async function () { BrowserTestUtils.startLoadingURIString(gBrowser, htmlContent); await loadedPromise; - await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function (arg) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { let frame = content.frames[0]; let sel = frame.getSelection(); let range = frame.document.createRange(); diff --git a/browser/base/content/test/general/browser_bug537013.js b/browser/base/content/test/general/browser_bug537013.js index 5c871a759c..58bcec9754 100644 --- a/browser/base/content/test/general/browser_bug537013.js +++ b/browser/base/content/test/general/browser_bug537013.js @@ -15,7 +15,7 @@ var HasFindClipboard = Services.clipboard.isClipboardTypeSupported( Services.clipboard.kFindClipboard ); -function addTabWithText(aText, aCallback) { +function addTabWithText(aText) { let newTab = BrowserTestUtils.addTab( gBrowser, "data:text/html;charset=utf-8,<h1 id='h1'>" + aText + "</h1>" diff --git a/browser/base/content/test/general/browser_bug565575.js b/browser/base/content/test/general/browser_bug565575.js index 6176c537e3..b974b17205 100644 --- a/browser/base/content/test/general/browser_bug565575.js +++ b/browser/base/content/test/general/browser_bug565575.js @@ -3,7 +3,7 @@ add_task(async function () { await BrowserTestUtils.openNewForegroundTab( gBrowser, - () => BrowserOpenTab(), + () => BrowserCommands.openTab(), false ); ok(gURLBar.focused, "location bar is focused for a new tab"); diff --git a/browser/base/content/test/general/browser_bug567306.js b/browser/base/content/test/general/browser_bug567306.js index 3d3e47e17d..24280371d8 100644 --- a/browser/base/content/test/general/browser_bug567306.js +++ b/browser/base/content/test/general/browser_bug567306.js @@ -10,7 +10,7 @@ add_task(async function () { let newwindow = await BrowserTestUtils.openNewBrowserWindow(); let selectedBrowser = newwindow.gBrowser.selectedBrowser; - await new Promise((resolve, reject) => { + await new Promise(resolve => { BrowserTestUtils.waitForContentEvent( selectedBrowser, "pageshow", diff --git a/browser/base/content/test/general/browser_bug609700.js b/browser/base/content/test/general/browser_bug609700.js index 8195eba4ec..615b63c3d8 100644 --- a/browser/base/content/test/general/browser_bug609700.js +++ b/browser/base/content/test/general/browser_bug609700.js @@ -1,11 +1,7 @@ function test() { waitForExplicitFinish(); - Services.ww.registerNotification(function notification( - aSubject, - aTopic, - aData - ) { + Services.ww.registerNotification(function notification(aSubject, aTopic) { if (aTopic == "domwindowopened") { Services.ww.unregisterNotification(notification); diff --git a/browser/base/content/test/general/browser_bug623893.js b/browser/base/content/test/general/browser_bug623893.js index 79cd10c591..0f742a8e8e 100644 --- a/browser/base/content/test/general/browser_bug623893.js +++ b/browser/base/content/test/general/browser_bug623893.js @@ -38,7 +38,7 @@ async function promiseGetIndex(browser) { return shistory.index; } -let duplicate = async function (delta, msg, cb) { +let duplicate = async function (delta, msg) { var startIndex = await promiseGetIndex(gBrowser.selectedBrowser); duplicateTabIn(gBrowser.selectedTab, "tab", delta); diff --git a/browser/base/content/test/general/browser_bug676619.js b/browser/base/content/test/general/browser_bug676619.js index 80bbce8cb0..90dd8f4f7c 100644 --- a/browser/base/content/test/general/browser_bug676619.js +++ b/browser/base/content/test/general/browser_bug676619.js @@ -22,7 +22,7 @@ function waitForNewWindow() { var domwindow = aXULWindow.docShell.domWindow; domwindow.addEventListener("load", downloadOnLoad, true); }, - onCloseWindow: aXULWindow => {}, + onCloseWindow: () => {}, }; Services.wm.addListener(listener); @@ -97,7 +97,7 @@ async function testLink(link, name) { } // Cross-origin URL does not trigger a download -async function testLocation(link, url) { +async function testLocation(link) { let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser); SpecialPowers.spawn(gBrowser.selectedBrowser, [link], contentLink => { @@ -116,8 +116,8 @@ async function runTest(url) { await BrowserTestUtils.browserLoaded(browser); await testLink("link1", "test.txt"); - await testLink("link2", "video.ogg"); - await testLink("link3", "just some video.ogg"); + await testLink("link2", "video.webm"); + await testLink("link3", "just some video.webm"); await testLink("link4", "with-target.txt"); await testLink("link5", "javascript.html"); await testLink("link6", "test.blob"); @@ -132,8 +132,8 @@ async function runTest(url) { // Check that we enforce the correct extension if the website's // is bogus or missing. These extensions can differ slightly (ogx vs ogg, // htm vs html) on different OSes. - let oggExtension = getMIMEInfoForType("application/ogg").primaryExtension; - await testLink("link13", "no file extension." + oggExtension); + let webmExtension = getMIMEInfoForType("video/webm").primaryExtension; + await testLink("link13", "no file extension." + webmExtension); // See https://bugzilla.mozilla.org/show_bug.cgi?id=1690051#c8 if (AppConstants.platform != "win") { diff --git a/browser/base/content/test/general/browser_bug734076.js b/browser/base/content/test/general/browser_bug734076.js index bd86f8e2b3..e9bec6834e 100644 --- a/browser/base/content/test/general/browser_bug734076.js +++ b/browser/base/content/test/general/browser_bug734076.js @@ -36,7 +36,7 @@ add_task(async function () { ); }, verify(browser) { - return SpecialPowers.spawn(browser, [], async function (arg) { + return SpecialPowers.spawn(browser, [], async function () { Assert.equal( content.document.body.textContent, "", @@ -67,7 +67,7 @@ add_task(async function () { ); }, verify(browser) { - return SpecialPowers.spawn(browser, [], async function (arg) { + return SpecialPowers.spawn(browser, [], async function () { Assert.equal( content.document.body.textContent, "", @@ -105,7 +105,7 @@ add_task(async function () { ); }, verify(browser) { - return SpecialPowers.spawn(browser, [], async function (arg) { + return SpecialPowers.spawn(browser, [], async function () { Assert.equal( content.document.body.textContent, "", diff --git a/browser/base/content/test/general/browser_bug763468_perwindowpb.js b/browser/base/content/test/general/browser_bug763468_perwindowpb.js index bed03561ca..05a7f90550 100644 --- a/browser/base/content/test/general/browser_bug763468_perwindowpb.js +++ b/browser/base/content/test/general/browser_bug763468_perwindowpb.js @@ -44,7 +44,7 @@ add_task(async function testPBNewTab() { async function openNewTab(aWindow, aExpectedURL) { // Open a new tab - aWindow.BrowserOpenTab(); + aWindow.BrowserCommands.openTab(); let browser = aWindow.gBrowser.selectedBrowser; // We're already loaded. diff --git a/browser/base/content/test/general/browser_bug767836_perwindowpb.js b/browser/base/content/test/general/browser_bug767836_perwindowpb.js index 7fcc6ad565..e237f1216d 100644 --- a/browser/base/content/test/general/browser_bug767836_perwindowpb.js +++ b/browser/base/content/test/general/browser_bug767836_perwindowpb.js @@ -59,7 +59,7 @@ add_task(async function test_newTabService() { async function openNewTab(aWindow, aExpectedURL) { // Open a new tab - aWindow.BrowserOpenTab(); + aWindow.BrowserCommands.openTab(); let browser = aWindow.gBrowser.selectedBrowser; // We're already loaded. diff --git a/browser/base/content/test/general/browser_bug817947.js b/browser/base/content/test/general/browser_bug817947.js index ea3c39222e..f83e07a9af 100644 --- a/browser/base/content/test/general/browser_bug817947.js +++ b/browser/base/content/test/general/browser_bug817947.js @@ -32,7 +32,7 @@ add_task(async () => { win.close(); }); -async function preparePendingTab(aCallback) { +async function preparePendingTab() { let tab = BrowserTestUtils.addTab(gBrowser, URL); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); diff --git a/browser/base/content/test/general/browser_clipboard.js b/browser/base/content/test/general/browser_clipboard.js index a4c823969f..7820c4ec89 100644 --- a/browser/base/content/test/general/browser_clipboard.js +++ b/browser/base/content/test/general/browser_clipboard.js @@ -68,7 +68,7 @@ add_task(async function () { let selection = content.document.getSelection(); selection.modify("move", "right", "line"); - return new Promise((resolve, reject) => { + return new Promise(resolve => { content.addEventListener( "paste", event => { @@ -130,7 +130,7 @@ add_task(async function () { selection.modify("extend", "left", "word"); selection.modify("extend", "left", "character"); - return new Promise((resolve, reject) => { + return new Promise(resolve => { content.addEventListener( "cut", event => { @@ -157,7 +157,7 @@ add_task(async function () { let selection = content.document.getSelection(); selection.modify("move", "left", "line"); - return new Promise((resolve, reject) => { + return new Promise(resolve => { content.addEventListener( "paste", event => { diff --git a/browser/base/content/test/general/browser_clipboard_pastefile.js b/browser/base/content/test/general/browser_clipboard_pastefile.js index f034883ef2..6ef3edf30e 100644 --- a/browser/base/content/test/general/browser_clipboard_pastefile.js +++ b/browser/base/content/test/general/browser_clipboard_pastefile.js @@ -50,7 +50,7 @@ add_task(async function () { ); let browser = tab.linkedBrowser; - let resultPromise = SpecialPowers.spawn(browser, [], function (arg) { + let resultPromise = SpecialPowers.spawn(browser, [], function () { return new Promise(resolve => { content.document.addEventListener("testresult", event => { resolve(event.detail.result); @@ -73,7 +73,7 @@ add_task(async function () { document.documentElement.appendChild(input); input.focus(); - await new Promise((resolve, reject) => { + await new Promise(resolve => { input.addEventListener( "paste", function (event) { diff --git a/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js b/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js index c96fa6cf7b..f44620c29e 100644 --- a/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js +++ b/browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js @@ -149,7 +149,7 @@ add_task(async function () { gBrowser.selectedBrowser, FS_CHANGE_SIZE ); - executeSoon(() => BrowserFullScreen()); + executeSoon(() => BrowserCommands.fullScreen()); await fullscreenPromise; } }); @@ -195,7 +195,7 @@ add_task(async function () { // dispatched synchronously, which would cause the event listener // miss that event and wait infinitely. fullscreenPromise = waitForFullscreenChanges(browser, FS_CHANGE_SIZE); - executeSoon(() => BrowserFullScreen()); + executeSoon(() => BrowserCommands.fullScreen()); contentStates = await fullscreenPromise; checkState({ inDOMFullscreen: false, inFullscreen: true }, contentStates); @@ -228,7 +228,7 @@ add_task(async function () { if (window.fullScreen) { info("> Cleanup"); fullscreenPromise = waitForFullscreenChanges(browser, FS_CHANGE_SIZE); - executeSoon(() => BrowserFullScreen()); + executeSoon(() => BrowserCommands.fullScreen()); await fullscreenPromise; } } diff --git a/browser/base/content/test/general/browser_double_close_tab.js b/browser/base/content/test/general/browser_double_close_tab.js index f5f2f1b6c7..6beea0f42b 100644 --- a/browser/base/content/test/general/browser_double_close_tab.js +++ b/browser/base/content/test/general/browser_double_close_tab.js @@ -18,7 +18,7 @@ function waitForDialog(callback) { function waitForDialogDestroyed(node, callback) { // Now listen for the dialog going away again... - let observer = new MutationObserver(function (muts) { + let observer = new MutationObserver(function () { if (!node.parentNode) { ok(true, "Dialog is gone"); done(); diff --git a/browser/base/content/test/general/browser_focusonkeydown.js b/browser/base/content/test/general/browser_focusonkeydown.js index 9cf1f113f5..53919bc1b3 100644 --- a/browser/base/content/test/general/browser_focusonkeydown.js +++ b/browser/base/content/test/general/browser_focusonkeydown.js @@ -20,7 +20,7 @@ add_task(async function () { gURLBar.addEventListener( "keydown", - function (event) { + function () { gBrowser.selectedBrowser.focus(); }, { capture: true, once: true } diff --git a/browser/base/content/test/general/browser_fullscreen-window-open.js b/browser/base/content/test/general/browser_fullscreen-window-open.js index 2b21e34e92..be1d2ca3a3 100644 --- a/browser/base/content/test/general/browser_fullscreen-window-open.js +++ b/browser/base/content/test/general/browser_fullscreen-window-open.js @@ -26,14 +26,14 @@ async function test() { await promiseTabLoadEvent(newBrowser.selectedTab, gHttpTestRoot + TEST_FILE); // Enter browser fullscreen mode. - newWin.BrowserFullScreen(); + newWin.BrowserCommands.fullScreen(); runNextTest(); } registerCleanupFunction(async function () { // Exit browser fullscreen mode. - newWin.BrowserFullScreen(); + newWin.BrowserCommands.fullScreen(); await BrowserTestUtils.closeWindow(newWin); @@ -336,7 +336,7 @@ WindowListener.prototype = { Services.wm.removeListener(this); let domwindow = aXULWindow.docShell.domWindow; - let onLoad = aEvent => { + let onLoad = () => { is( domwindow.document.location.href, this.test_url, @@ -361,6 +361,6 @@ WindowListener.prototype = { }; domwindow.addEventListener("load", onLoad, true); }, - onCloseWindow(aXULWindow) {}, + onCloseWindow() {}, QueryInterface: ChromeUtils.generateQI(["nsIWindowMediatorListener"]), }; diff --git a/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js b/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js index 1624a1514d..fae1130685 100644 --- a/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js +++ b/browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js @@ -33,7 +33,7 @@ add_task(async function checkBackFromInvalidURI() { false, // Be paranoid we *are* actually seeing this other page load, not some kind of race // for if/when we do start firing pageshow for the error page... - function (e) { + function () { return gBrowser.currentURI.spec == "about:robots"; } ); diff --git a/browser/base/content/test/general/browser_newWindowDrop.js b/browser/base/content/test/general/browser_newWindowDrop.js index 3e41b0d6ac..445999befd 100644 --- a/browser/base/content/test/general/browser_newWindowDrop.js +++ b/browser/base/content/test/general/browser_newWindowDrop.js @@ -184,7 +184,7 @@ function dropText(text, expectedURLs, ignoreFirstWindow = false) { ); } -async function drop(dragData, expectedURLs, ignoreFirstWindow = false) { +async function drop(dragData, expectedURLs) { let dragDataString = JSON.stringify(dragData); info( `Starting test for dragData:${dragDataString}; expectedURLs.length:${expectedURLs.length}` diff --git a/browser/base/content/test/general/browser_plainTextLinks.js b/browser/base/content/test/general/browser_plainTextLinks.js index 706f21387c..44c9b8422b 100644 --- a/browser/base/content/test/general/browser_plainTextLinks.js +++ b/browser/base/content/test/general/browser_plainTextLinks.js @@ -19,7 +19,7 @@ add_task(async function () { await SimpleTest.promiseFocus(gBrowser.selectedBrowser); // Initial setup of the content area. - await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function (arg) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { let doc = content.document; let range = doc.createRange(); let selection = content.getSelection(); diff --git a/browser/base/content/test/general/browser_private_no_prompt.js b/browser/base/content/test/general/browser_private_no_prompt.js index d8c9f8e7b5..80ba0ca746 100644 --- a/browser/base/content/test/general/browser_private_no_prompt.js +++ b/browser/base/content/test/general/browser_private_no_prompt.js @@ -3,8 +3,8 @@ function test() { var privateWin = OpenBrowserWindow({ private: true }); whenDelayedStartupFinished(privateWin, function () { - privateWin.BrowserOpenTab(); - privateWin.BrowserTryToCloseWindow(); + privateWin.BrowserCommands.openTab(); + privateWin.BrowserCommands.tryToCloseWindow(); ok(true, "didn't prompt"); executeSoon(finish); diff --git a/browser/base/content/test/general/browser_remoteTroubleshoot.js b/browser/base/content/test/general/browser_remoteTroubleshoot.js index 84722b2603..55627f0b28 100644 --- a/browser/base/content/test/general/browser_remoteTroubleshoot.js +++ b/browser/base/content/test/general/browser_remoteTroubleshoot.js @@ -19,9 +19,9 @@ const TEST_URI_GOOD_OBJECT = Services.io.newURI( // Creates a one-shot web-channel for the test data to be sent back from the test page. function promiseChannelResponse(channelID, originOrPermission) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { let channel = new WebChannel(channelID, originOrPermission); - channel.listen((id, data, target) => { + channel.listen((id, data) => { channel.stopListening(); resolve(data); }); diff --git a/browser/base/content/test/general/browser_save_link-perwindowpb.js b/browser/base/content/test/general/browser_save_link-perwindowpb.js index d5a0eef86c..29fd54d2d9 100644 --- a/browser/base/content/test/general/browser_save_link-perwindowpb.js +++ b/browser/base/content/test/general/browser_save_link-perwindowpb.js @@ -68,7 +68,7 @@ function triggerSave(aWindow, aCallback) { info("popup hidden"); } - function onTransferComplete(aWindow2, downloadSuccess, destDir) { + function onTransferComplete(aWindow2, downloadSuccess) { ok(downloadSuccess, "Link should have been downloaded successfully"); aWindow2.close(); @@ -118,7 +118,7 @@ function test() { info("Finished running the cleanup code"); }); - function observer(subject, topic, state) { + function observer(subject, topic) { info("observer called with " + topic); if (topic == "http-on-modify-request") { onModifyRequest(subject); diff --git a/browser/base/content/test/general/browser_save_link_when_window_navigates.js b/browser/base/content/test/general/browser_save_link_when_window_navigates.js index 1c68b91ddf..e5c7fa76b2 100644 --- a/browser/base/content/test/general/browser_save_link_when_window_navigates.js +++ b/browser/base/content/test/general/browser_save_link_when_window_navigates.js @@ -70,7 +70,7 @@ function triggerSave(aWindow, aCallback) { info("done mockTransferCallback"); }; - function onUCTDialog(dialog) { + function onUCTDialog() { SpecialPowers.spawn(testBrowser, [], async () => { content.document.querySelector("iframe").remove(); }).then(() => executeSoon(continueDownloading)); @@ -104,7 +104,7 @@ var windowObserver = { } this._callback = aCallback; }, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic != "domwindowopened") { return; } @@ -113,7 +113,7 @@ var windowObserver = { win.addEventListener( "load", - function (event) { + function () { if (win.location == UCT_URI) { SimpleTest.executeSoon(function () { if (windowObserver._callback) { diff --git a/browser/base/content/test/general/browser_save_private_link_perwindowpb.js b/browser/base/content/test/general/browser_save_private_link_perwindowpb.js index 42632bdc5a..1312c7b954 100644 --- a/browser/base/content/test/general/browser_save_private_link_perwindowpb.js +++ b/browser/base/content/test/general/browser_save_private_link_perwindowpb.js @@ -12,9 +12,9 @@ function createTemporarySaveDirectory() { } function promiseNoCacheEntry(filename) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { Visitor.prototype = { - onCacheStorageInfo(num, consumption) { + onCacheStorageInfo(num) { info("disk storage contains " + num + " entries"); }, onCacheEntryInfo(uri) { @@ -40,7 +40,7 @@ function promiseNoCacheEntry(filename) { } function promiseImageDownloaded() { - return new Promise((resolve, reject) => { + return new Promise(resolve => { let fileName; let MockFilePicker = SpecialPowers.MockFilePicker; MockFilePicker.init(window.browsingContext); diff --git a/browser/base/content/test/general/browser_save_video.js b/browser/base/content/test/general/browser_save_video.js index d8dc5c6e2e..f0450ac1fa 100644 --- a/browser/base/content/test/general/browser_save_video.js +++ b/browser/base/content/test/general/browser_save_video.js @@ -52,7 +52,7 @@ add_task(async function () { is( fileName, - "web-video1-expectedName.ogv", + "web-video1-expectedName.webm", "Video file name is correctly retrieved from Content-Disposition http header" ); resolve(); diff --git a/browser/base/content/test/general/browser_tabfocus.js b/browser/base/content/test/general/browser_tabfocus.js index b057a504e5..7cc9158084 100644 --- a/browser/base/content/test/general/browser_tabfocus.js +++ b/browser/base/content/test/general/browser_tabfocus.js @@ -322,7 +322,7 @@ add_task(async function () { "tab change when selected tab element was focused" ); - let switchWaiter = new Promise((resolve, reject) => { + let switchWaiter = new Promise(resolve => { gBrowser.addEventListener( "TabSwitchDone", function () { @@ -516,7 +516,7 @@ add_task(async function () { // now go back again gURLBar.focus(); - await new Promise((resolve, reject) => { + await new Promise(resolve => { BrowserTestUtils.waitForContentEvent( window.gBrowser.selectedBrowser, "pageshow", diff --git a/browser/base/content/test/general/browser_tabs_owner.js b/browser/base/content/test/general/browser_tabs_owner.js index 4a32da12f1..e214b861e8 100644 --- a/browser/base/content/test/general/browser_tabs_owner.js +++ b/browser/base/content/test/general/browser_tabs_owner.js @@ -8,13 +8,13 @@ function test() { is(gBrowser.tabs.length, 4, "4 tabs are open"); owner = gBrowser.selectedTab = gBrowser.tabs[2]; - BrowserOpenTab(); + BrowserCommands.openTab(); is(gBrowser.selectedTab, gBrowser.tabs[4], "newly opened tab is selected"); gBrowser.removeCurrentTab(); is(gBrowser.selectedTab, owner, "owner is selected"); owner = gBrowser.selectedTab; - BrowserOpenTab(); + BrowserCommands.openTab(); gBrowser.selectedTab = gBrowser.tabs[1]; gBrowser.selectedTab = gBrowser.tabs[4]; gBrowser.removeCurrentTab(); @@ -25,7 +25,7 @@ function test() { ); owner = gBrowser.selectedTab; - BrowserOpenTab(); + BrowserCommands.openTab(); gBrowser.moveTabTo(gBrowser.selectedTab, 0); gBrowser.removeCurrentTab(); is( diff --git a/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js b/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js index 6c62670e6f..26c040324a 100644 --- a/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js +++ b/browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js @@ -1,7 +1,7 @@ function wait_while_tab_is_busy() { return new Promise(resolve => { let progressListener = { - onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aWebProgress, aRequest, aStateFlags) { if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { gBrowser.removeProgressListener(this); setTimeout(resolve, 0); @@ -27,7 +27,7 @@ var with_new_tab_opened = async function (options, taskFn) { }; add_task(async function test_regular_page() { - function test_expect_view_source_enabled(browser) { + function test_expect_view_source_enabled() { for (let element of [...XULBrowserWindow._elementsForViewSource]) { ok(!element.hasAttribute("disabled"), "View Source should be enabled"); } @@ -44,7 +44,7 @@ add_task(async function test_regular_page() { }); add_task(async function test_view_source_page() { - function test_expect_view_source_disabled(browser) { + function test_expect_view_source_disabled() { for (let element of [...XULBrowserWindow._elementsForViewSource]) { ok(element.hasAttribute("disabled"), "View Source should be disabled"); } diff --git a/browser/base/content/test/general/browser_zbug569342.js b/browser/base/content/test/general/browser_zbug569342.js index 4aa6bfbb9c..0c30ff3d1d 100644 --- a/browser/base/content/test/general/browser_zbug569342.js +++ b/browser/base/content/test/general/browser_zbug569342.js @@ -57,7 +57,7 @@ function testFindDisabled(url) { } async function testFindEnabled(url) { - return BrowserTestUtils.withNewTab(url, async function (browser) { + return BrowserTestUtils.withNewTab(url, async function () { ok( !document.getElementById("cmd_find").getAttribute("disabled"), "Find command should not be disabled" diff --git a/browser/base/content/test/general/download_page.html b/browser/base/content/test/general/download_page.html index 300bacdb72..625ff46aab 100644 --- a/browser/base/content/test/general/download_page.html +++ b/browser/base/content/test/general/download_page.html @@ -13,10 +13,10 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=676619 <ul> <li><a href="download_page_1.txt" download="test.txt" id="link1">Download "test.txt"</a></li> - <li><a href="video.ogg" - download id="link2">Download "video.ogg"</a></li> - <li><a href="video.ogg" - download="just some video.ogg" id="link3">Download "just some video.ogg"</a></li> + <li><a href="video.webm" + download id="link2">Download "video.webm"</a></li> + <li><a href="video.webm" + download="just some video.webm" id="link3">Download "just some video.webm"</a></li> <li><a href="download_page_2.txt" download="with-target.txt" id="link4">Download "with-target.txt"</a></li> <li><a href="javascript:(1+2)+''" @@ -33,7 +33,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=676619 download="download_page_4.txt" id="link11">Download "download_page_4.txt"</a></li> <li><a href="http://example.com/" download="example.com" id="link12" target="_blank">Download "example.com"</a></li> - <li><a href="video.ogg" + <li><a href="video.webm" download="no file extension" id="link13">Download "force extension"</a></li> <li><a href="dummy.ics" download="dummy.not-ics" id="link14">Download "dummy.not-ics"</a></li> @@ -64,7 +64,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=676619 "wrong-file-name", {type: "application/x-some-file"})); document.getElementById("link7").href = fileURL; - window.addEventListener("beforeunload", function(evt) { + window.addEventListener("beforeunload", function() { document.getElementById("unload-flag").textContent = "Fail"; }); </script> diff --git a/browser/base/content/test/general/head.js b/browser/base/content/test/general/head.js index f7b4a0d93b..94f60f2936 100644 --- a/browser/base/content/test/general/head.js +++ b/browser/base/content/test/general/head.js @@ -165,7 +165,7 @@ function promiseOpenAndLoadWindow(aOptions, aWaitForDelayedStartup = false) { return new Promise(resolve => { let win = OpenBrowserWindow(aOptions); if (aWaitForDelayedStartup) { - Services.obs.addObserver(function onDS(aSubject, aTopic, aData) { + Services.obs.addObserver(function onDS(aSubject) { if (aSubject != win) { return; } @@ -185,7 +185,7 @@ function promiseOpenAndLoadWindow(aOptions, aWaitForDelayedStartup = false) { } async function whenNewTabLoaded(aWindow, aCallback) { - aWindow.BrowserOpenTab(); + aWindow.BrowserCommands.openTab(); let expectedURL = AboutNewTab.newTabURL; let browser = aWindow.gBrowser.selectedBrowser; diff --git a/browser/base/content/test/general/video.ogg b/browser/base/content/test/general/video.ogg Binary files differdeleted file mode 100644 index ac7ece3519..0000000000 --- a/browser/base/content/test/general/video.ogg +++ /dev/null diff --git a/browser/base/content/test/general/video.webm b/browser/base/content/test/general/video.webm Binary files differnew file mode 100644 index 0000000000..0ca38d3cf0 --- /dev/null +++ b/browser/base/content/test/general/video.webm diff --git a/browser/base/content/test/general/web_video.html b/browser/base/content/test/general/web_video.html index 467fb0ce1c..e03c85d1dc 100644 --- a/browser/base/content/test/general/web_video.html +++ b/browser/base/content/test/general/web_video.html @@ -5,6 +5,6 @@ <body> This document has some web video in it. <br> - <video src="web_video1.ogv" id="video1"> </video> + <video src="web_video1.webm" id="video1"> </video> </body> </html> diff --git a/browser/base/content/test/general/web_video1.ogv b/browser/base/content/test/general/web_video1.ogv Binary files differdeleted file mode 100644 index 093158432a..0000000000 --- a/browser/base/content/test/general/web_video1.ogv +++ /dev/null diff --git a/browser/base/content/test/general/web_video1.ogv^headers^ b/browser/base/content/test/general/web_video1.ogv^headers^ deleted file mode 100644 index 4511e92552..0000000000 --- a/browser/base/content/test/general/web_video1.ogv^headers^ +++ /dev/null @@ -1,3 +0,0 @@ -Content-Disposition: filename="web-video1-expectedName.ogv" -Content-Type: video/ogg - diff --git a/browser/base/content/test/general/web_video1.webm b/browser/base/content/test/general/web_video1.webm Binary files differnew file mode 100644 index 0000000000..2c9d7dad8d --- /dev/null +++ b/browser/base/content/test/general/web_video1.webm diff --git a/browser/base/content/test/general/web_video1.webm^headers^ b/browser/base/content/test/general/web_video1.webm^headers^ new file mode 100644 index 0000000000..d027132ea2 --- /dev/null +++ b/browser/base/content/test/general/web_video1.webm^headers^ @@ -0,0 +1,3 @@ +Content-Disposition: filename="web-video1-expectedName.webm" +Content-Type: video/webm + diff --git a/browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js b/browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js index a5910964e7..eeeb7e8b9e 100644 --- a/browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js +++ b/browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js @@ -5,7 +5,7 @@ function test() { waitForExplicitFinish(); - BrowserOpenTab(); + BrowserCommands.openTab(); let tab = gBrowser.selectedTab; registerCleanupFunction(function () { gBrowser.removeTab(tab); diff --git a/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js b/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js index 8640716bab..ef92d4c528 100644 --- a/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js +++ b/browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js @@ -12,7 +12,7 @@ const kDevPanelID = "PanelUI-developer-tools"; function waitForLocationChange() { let promise = new Promise(resolve => { let wpl = { - onLocationChange(aWebProgress, aRequest, aLocation) { + onLocationChange() { gBrowser.removeProgressListener(wpl); resolve(); }, @@ -213,31 +213,23 @@ add_task(async function testSidebarsButtonPress() { // This is an image with a click handler on its parent and no command handler, // but the toolbar keyboard navigation code should handle keyboard activation. add_task(async function testBookmarkButtonPress() { - await BrowserTestUtils.withNewTab( - "https://example.com", - async function (aBrowser) { - let button = document.getElementById("star-button-box"); - StarUI._createPanelIfNeeded(); - let panel = document.getElementById("editBookmarkPanel"); - let focused = BrowserTestUtils.waitForEvent(panel, "focus", true); - // The button ignores activation while the bookmarked status is being - // updated. So, wait for it to finish updating. - await TestUtils.waitForCondition( - () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING - ); - await focusAndActivateElement(button, () => - EventUtils.synthesizeKey(" ") - ); - await focused; - ok( - true, - "Focus inside edit bookmark panel after Bookmark button pressed" - ); - let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); - EventUtils.synthesizeKey("KEY_Escape"); - await hidden; - } - ); + await BrowserTestUtils.withNewTab("https://example.com", async function () { + let button = document.getElementById("star-button-box"); + StarUI._createPanelIfNeeded(); + let panel = document.getElementById("editBookmarkPanel"); + let focused = BrowserTestUtils.waitForEvent(panel, "focus", true); + // The button ignores activation while the bookmarked status is being + // updated. So, wait for it to finish updating. + await TestUtils.waitForCondition( + () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING + ); + await focusAndActivateElement(button, () => EventUtils.synthesizeKey(" ")); + await focused; + ok(true, "Focus inside edit bookmark panel after Bookmark button pressed"); + let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + EventUtils.synthesizeKey("KEY_Escape"); + await hidden; + }); }); // Test activation of the Bookmarks Menu button from the keyboard. @@ -302,33 +294,24 @@ add_task(async function testDownloadsButtonPress() { // with a browser element to embed the pocket UI into it. // The Pocket panel should appear and focus should move inside it. add_task(async function testPocketButtonPress() { - await BrowserTestUtils.withNewTab( - "https://example.com", - async function (aBrowser) { - let button = document.getElementById("save-to-pocket-button"); - // The panel is created on the fly, so we can't simply wait for focus - // inside it. - let showing = BrowserTestUtils.waitForEvent( - document, - "popupshowing", - true - ); - await focusAndActivateElement(button, () => - EventUtils.synthesizeKey(" ") - ); - let event = await showing; - let panel = event.target; - is(panel.id, "customizationui-widget-panel"); - let focused = BrowserTestUtils.waitForEvent(panel, "focus", true); - await focused; - is( - document.activeElement.tagName, - "browser", - "Focus inside Pocket panel after Bookmark button pressed" - ); - let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); - EventUtils.synthesizeKey("KEY_Escape"); - await hidden; - } - ); + await BrowserTestUtils.withNewTab("https://example.com", async function () { + let button = document.getElementById("save-to-pocket-button"); + // The panel is created on the fly, so we can't simply wait for focus + // inside it. + let showing = BrowserTestUtils.waitForEvent(document, "popupshowing", true); + await focusAndActivateElement(button, () => EventUtils.synthesizeKey(" ")); + let event = await showing; + let panel = event.target; + is(panel.id, "customizationui-widget-panel"); + let focused = BrowserTestUtils.waitForEvent(panel, "focus", true); + await focused; + is( + document.activeElement.tagName, + "browser", + "Focus inside Pocket panel after Bookmark button pressed" + ); + let hidden = BrowserTestUtils.waitForEvent(panel, "popuphidden"); + EventUtils.synthesizeKey("KEY_Escape"); + await hidden; + }); }); diff --git a/browser/base/content/test/metaTags/browser_bad_meta_tags.js b/browser/base/content/test/metaTags/browser_bad_meta_tags.js index 00cc128ec0..aa025725dc 100644 --- a/browser/base/content/test/metaTags/browser_bad_meta_tags.js +++ b/browser/base/content/test/metaTags/browser_bad_meta_tags.js @@ -9,11 +9,12 @@ const TEST_PATH = ) + "bad_meta_tags.html"; /** - * This tests that with the page bad_meta_tags.html, ContentMetaHandler.jsm parses - * out the meta tags available and does not store content provided by a malformed - * meta tag. In this case the best defined meta tags are malformed, so here we - * test that we store the next best ones - "description" and "twitter:image". The - * list of meta tags and order of preference is found in ContentMetaHandler.jsm. + * This tests that with the page bad_meta_tags.html, ContentMetaHandler.sys.mjs + * parses out the meta tags available and does not store content provided by a + * malformed meta tag. In this case the best defined meta tags are malformed, so + * here we test that we store the next best ones - "description" and "twitter:image". + * The list of meta tags and order of preference is found in + * ContentMetaHandler.sys.mjs. */ add_task(async function test_bad_meta_tags() { const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PATH); diff --git a/browser/base/content/test/metaTags/browser_meta_tags.js b/browser/base/content/test/metaTags/browser_meta_tags.js index 380a71214c..870860dc18 100644 --- a/browser/base/content/test/metaTags/browser_meta_tags.js +++ b/browser/base/content/test/metaTags/browser_meta_tags.js @@ -8,11 +8,11 @@ const TEST_PATH = "https://example.com" ) + "meta_tags.html"; /** - * This tests that with the page meta_tags.html, ContentMetaHandler.jsm parses - * out the meta tags avilable and only stores the best one for description and - * one for preview image url. In the case of this test, the best defined meta + * This tests that with the page meta_tags.html, ContentMetaHandler.sys.mjs + * parses out the meta tags avilable and only stores the best one for description + * and one for preview image url. In the case of this test, the best defined meta * tags are "og:description" and "og:image:secure_url". The list of meta tags - * and order of preference is found in ContentMetaHandler.jsm. + * and order of preference is found in ContentMetaHandler.sys.mjs. */ add_task(async function test_metadata() { const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PATH); diff --git a/browser/base/content/test/outOfProcess/browser_basic_outofprocess.js b/browser/base/content/test/outOfProcess/browser_basic_outofprocess.js index 50914a286c..41fc96986e 100644 --- a/browser/base/content/test/outOfProcess/browser_basic_outofprocess.js +++ b/browser/base/content/test/outOfProcess/browser_basic_outofprocess.js @@ -121,12 +121,9 @@ add_task(async function test_subframes_function() { let browser = tab.linkedBrowser; let counter = 0; - let browsingContexts = await initChildFrames( - browser, - function (browsingContext) { - return "<p>Text " + ++counter + "</p>"; - } - ); + let browsingContexts = await initChildFrames(browser, function () { + return "<p>Text " + ++counter + "</p>"; + }); is( counter, diff --git a/browser/base/content/test/pageActions/head.js b/browser/base/content/test/pageActions/head.js index cd269bf9b5..370f01734c 100644 --- a/browser/base/content/test/pageActions/head.js +++ b/browser/base/content/test/pageActions/head.js @@ -124,7 +124,7 @@ async function promiseAnimationFrame(win = window) { async function promisePopupNotShown(id, win = window) { let deferred = Promise.withResolvers(); - function listener(e) { + function listener() { deferred.reject("Unexpected popupshown"); } let panel = win.document.getElementById(id); diff --git a/browser/base/content/test/pageinfo/browser.toml b/browser/base/content/test/pageinfo/browser.toml index ae70eb68ff..9e14392450 100644 --- a/browser/base/content/test/pageinfo/browser.toml +++ b/browser/base/content/test/pageinfo/browser.toml @@ -5,7 +5,7 @@ support-files = [ "image.html", "../general/audio.ogg", "../general/moz.png", - "../general/video.ogg", + "../general/video.webm", ] ["browser_pageinfo_iframe_media.js"] diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js b/browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js index 354e85a241..dbd5d8fe25 100644 --- a/browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js +++ b/browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js @@ -76,7 +76,7 @@ async function test() { // Pass a dummy imageElement, if there isn't an imageElement, pageInfo.js // will do a preview, however this sometimes will cause intermittent failures, // see bug 1403365. - let pageInfo = BrowserPageInfo(url, "mediaTab", {}); + let pageInfo = BrowserCommands.pageInfo(url, "mediaTab", {}); info("waitForEvent pageInfo"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js b/browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js index 7550379ad1..3dad0a50f3 100644 --- a/browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js +++ b/browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js @@ -10,7 +10,7 @@ add_task(async function test_all_images_mentioned() { await BrowserTestUtils.withNewTab( TEST_PATH + "iframes.html", async function () { - let pageInfo = BrowserPageInfo( + let pageInfo = BrowserCommands.pageInfo( gBrowser.selectedBrowser.currentURI.spec, "mediaTab" ); diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js b/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js index 374cd5f032..0fea68d640 100644 --- a/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js +++ b/browser/base/content/test/pageinfo/browser_pageinfo_image_info.js @@ -28,7 +28,7 @@ add_task(async function () { }; }); - let pageInfo = BrowserPageInfo( + let pageInfo = BrowserCommands.pageInfo( browser.currentURI.spec, "mediaTab", imageInfo diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_images.js b/browser/base/content/test/pageinfo/browser_pageinfo_images.js index e1f71204d0..c356c1c690 100644 --- a/browser/base/content/test/pageinfo/browser_pageinfo_images.js +++ b/browser/base/content/test/pageinfo/browser_pageinfo_images.js @@ -10,7 +10,7 @@ add_task(async function test_all_images_mentioned() { await BrowserTestUtils.withNewTab( TEST_PATH + "all_images.html", async function () { - let pageInfo = BrowserPageInfo( + let pageInfo = BrowserCommands.pageInfo( gBrowser.selectedBrowser.currentURI.spec, "mediaTab" ); @@ -97,7 +97,7 @@ add_task(async function test_image_size() { await BrowserTestUtils.withNewTab( TEST_PATH + "all_images.html", async function () { - let pageInfo = BrowserPageInfo( + let pageInfo = BrowserCommands.pageInfo( gBrowser.selectedBrowser.currentURI.spec, "mediaTab" ); diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js b/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js index ebf027811d..6b11ac19b9 100644 --- a/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js +++ b/browser/base/content/test/pageinfo/browser_pageinfo_permissions.js @@ -7,8 +7,8 @@ const TEST_ORIGIN_CERT_ERROR = "https://expired.example.com"; const LOW_TLS_VERSION = "https://tls1.example.com/"; async function testPermissions(defaultPermission) { - await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) { - let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab"); + await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function () { + let pageInfo = BrowserCommands.pageInfo(TEST_ORIGIN, "permTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let defaultCheckbox = await TestUtils.waitForCondition(() => @@ -94,7 +94,7 @@ add_task(async function test_CertificateError() { await pageLoaded; - let pageInfo = BrowserPageInfo(TEST_ORIGIN_CERT_ERROR, "permTab"); + let pageInfo = BrowserCommands.pageInfo(TEST_ORIGIN_CERT_ERROR, "permTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let permissionTab = pageInfo.document.getElementById("permTab"); await TestUtils.waitForCondition( @@ -145,7 +145,7 @@ add_task(async function test_NetworkError() { await pageLoaded; - let pageInfo = BrowserPageInfo(LOW_TLS_VERSION, "permTab"); + let pageInfo = BrowserCommands.pageInfo(LOW_TLS_VERSION, "permTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let permissionTab = pageInfo.document.getElementById("permTab"); await TestUtils.waitForCondition( @@ -192,8 +192,8 @@ add_task(async function test_default_geo_permission() { // Test special behavior for cookie permissions. add_task(async function test_cookie_permission() { - await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) { - let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab"); + await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function () { + let pageInfo = BrowserCommands.pageInfo(TEST_ORIGIN, "permTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let defaultCheckbox = await TestUtils.waitForCondition(() => diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_rtl.js b/browser/base/content/test/pageinfo/browser_pageinfo_rtl.js index d0c06a03ff..677a58516e 100644 --- a/browser/base/content/test/pageinfo/browser_pageinfo_rtl.js +++ b/browser/base/content/test/pageinfo/browser_pageinfo_rtl.js @@ -1,18 +1,15 @@ async function testPageInfo() { - await BrowserTestUtils.withNewTab( - "https://example.com", - async function (browser) { - let pageInfo = BrowserPageInfo(); - await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); - is( - getComputedStyle(pageInfo.document.documentElement).direction, - "rtl", - "Should be RTL" - ); - ok(true, "Didn't assert or crash"); - pageInfo.close(); - } - ); + await BrowserTestUtils.withNewTab("https://example.com", async function () { + let pageInfo = BrowserCommands.pageInfo(); + await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); + is( + getComputedStyle(pageInfo.document.documentElement).direction, + "rtl", + "Should be RTL" + ); + ok(true, "Didn't assert or crash"); + pageInfo.close(); + }); } add_task(async function test_page_info_rtl() { diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_security.js b/browser/base/content/test/pageinfo/browser_pageinfo_security.js index 47df97db06..17ff0b9b75 100644 --- a/browser/base/content/test/pageinfo/browser_pageinfo_security.js +++ b/browser/base/content/test/pageinfo/browser_pageinfo_security.js @@ -24,7 +24,7 @@ add_task(async function test_ShowCertificate() { TEST_SUB_ORIGIN ); - let pageInfo = BrowserPageInfo(TEST_SUB_ORIGIN, "securityTab"); + let pageInfo = BrowserCommands.pageInfo(TEST_SUB_ORIGIN, "securityTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let pageInfoDoc = pageInfo.document; let securityTab = pageInfoDoc.getElementById("securityTab"); @@ -74,7 +74,7 @@ add_task(async function test_image() { let url = TEST_PATH + "moz.png"; await BrowserTestUtils.openNewForegroundTab(gBrowser, url); - let pageInfo = BrowserPageInfo(url, "securityTab"); + let pageInfo = BrowserCommands.pageInfo(url, "securityTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let pageInfoDoc = pageInfo.document; let securityTab = pageInfoDoc.getElementById("securityTab"); @@ -128,7 +128,10 @@ add_task(async function test_CertificateError() { await pageLoaded; - let pageInfo = BrowserPageInfo(TEST_ORIGIN_CERT_ERROR, "securityTab"); + let pageInfo = BrowserCommands.pageInfo( + TEST_ORIGIN_CERT_ERROR, + "securityTab" + ); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let pageInfoDoc = pageInfo.document; let securityTab = pageInfoDoc.getElementById("securityTab"); @@ -165,7 +168,7 @@ add_task(async function test_CertificateError() { add_task(async function test_SecurityHTTP() { await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_HTTP_ORIGIN); - let pageInfo = BrowserPageInfo(TEST_HTTP_ORIGIN, "securityTab"); + let pageInfo = BrowserCommands.pageInfo(TEST_HTTP_ORIGIN, "securityTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let pageInfoDoc = pageInfo.document; let securityTab = pageInfoDoc.getElementById("securityTab"); @@ -201,7 +204,7 @@ add_task(async function test_SecurityHTTP() { add_task(async function test_ValidCert() { await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_ORIGIN); - let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab"); + let pageInfo = BrowserCommands.pageInfo(TEST_ORIGIN, "securityTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let pageInfoDoc = pageInfo.document; let securityTab = pageInfoDoc.getElementById("securityTab"); @@ -237,11 +240,11 @@ add_task(async function test_ValidCert() { add_task(async function test_SiteData() { await SiteDataTestUtils.addToIndexedDB(TEST_ORIGIN); - await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) { + await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function () { let totalUsage = await SiteDataTestUtils.getQuotaUsage(TEST_ORIGIN); Assert.greater(totalUsage, 0, "The total usage should not be 0"); - let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab"); + let pageInfo = BrowserCommands.pageInfo(TEST_ORIGIN, "securityTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let pageInfoDoc = pageInfo.document; @@ -302,8 +305,8 @@ add_task(async function test_Cookies() { value: "1", }); - await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) { - let pageInfo = BrowserPageInfo(TEST_ORIGIN, "securityTab"); + await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function () { + let pageInfo = BrowserCommands.pageInfo(TEST_ORIGIN, "securityTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let pageInfoDoc = pageInfo.document; diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_separate_private.js b/browser/base/content/test/pageinfo/browser_pageinfo_separate_private.js index ac93b7ddb2..74289107a8 100644 --- a/browser/base/content/test/pageinfo/browser_pageinfo_separate_private.js +++ b/browser/base/content/test/pageinfo/browser_pageinfo_separate_private.js @@ -9,7 +9,7 @@ add_task(async function () { "https://example.com" ); let browser = tab.linkedBrowser; - let pageInfo = BrowserPageInfo(browser.currentURI.spec); + let pageInfo = BrowserCommands.pageInfo(browser.currentURI.spec); await BrowserTestUtils.waitForEvent(pageInfo, "page-info-init"); Assert.strictEqual( pageInfo.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing, @@ -25,7 +25,7 @@ add_task(async function () { "https://example.com" ); let privateBrowser = privateTab.linkedBrowser; - let privatePageInfo = privateWindow.BrowserPageInfo( + let privatePageInfo = privateWindow.BrowserCommands.pageInfo( privateBrowser.currentURI.spec ); await BrowserTestUtils.waitForEvent(privatePageInfo, "page-info-init"); diff --git a/browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js b/browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js index 3934cd2aea..b00df72851 100644 --- a/browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js +++ b/browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js @@ -7,7 +7,7 @@ add_task(async function () { BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, URI); await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, URI); - const pageInfo = BrowserPageInfo( + const pageInfo = BrowserCommands.pageInfo( gBrowser.selectedBrowser.currentURI.spec, "mediaTab" ); diff --git a/browser/base/content/test/pageinfo/image.html b/browser/base/content/test/pageinfo/image.html index 1261be8e7b..35e2d78e1e 100644 --- a/browser/base/content/test/pageinfo/image.html +++ b/browser/base/content/test/pageinfo/image.html @@ -1,5 +1,5 @@ <html> <img src='moz.png' height=100 width=150 id='test-image'> - <video src='video.ogg' id='test-video'></video> + <video src='video.webm' id='test-video'></video> <audio src='audio.ogg' id='test-audio'></audio> </html> diff --git a/browser/base/content/test/performance/StartupContentSubframe.sys.mjs b/browser/base/content/test/performance/StartupContentSubframe.sys.mjs index a78e456afb..7d2d711765 100644 --- a/browser/base/content/test/performance/StartupContentSubframe.sys.mjs +++ b/browser/base/content/test/performance/StartupContentSubframe.sys.mjs @@ -16,7 +16,7 @@ export class StartupContentSubframeParent extends JSWindowActorParent { } export class StartupContentSubframeChild extends JSWindowActorChild { - async handleEvent(event) { + async handleEvent() { // When the remote subframe is loaded, an event will be fired to this actor, // which will cause us to send the `LoadedScripts` message to the parent // process. diff --git a/browser/base/content/test/performance/browser_preferences_usage.js b/browser/base/content/test/performance/browser_preferences_usage.js index 6bc623a360..b0ff140d19 100644 --- a/browser/base/content/test/performance/browser_preferences_usage.js +++ b/browser/base/content/test/performance/browser_preferences_usage.js @@ -107,7 +107,7 @@ add_task(async function startup() { "browser.startup.record": { // This pref is accessed in Nighly and debug builds only. min: 200, - max: 400, + max: 450, }, "network.loadinfo.skip_type_assertion": { // This is accessed in debug only. diff --git a/browser/base/content/test/performance/browser_startup_content.js b/browser/base/content/test/performance/browser_startup_content.js index b0f861e47f..fac82ad990 100644 --- a/browser/base/content/test/performance/browser_startup_content.js +++ b/browser/base/content/test/performance/browser_startup_content.js @@ -57,18 +57,11 @@ const known_scripts = { ]), }; -if (!Services.appinfo.sessionHistoryInParent) { - known_scripts.modules.add( - "resource:///modules/sessionstore/ContentSessionStore.sys.mjs" - ); -} - // Items on this list *might* load when creating the process, as opposed to // items in the main list, which we expect will always load. const intermittently_loaded_scripts = { modules: new Set([ "resource://gre/modules/nsAsyncShutdown.sys.mjs", - "resource://gre/modules/sessionstore/Utils.sys.mjs", // Translations code which may be preffed on. "resource://gre/actors/TranslationsChild.sys.mjs", @@ -119,7 +112,8 @@ add_task(async function () { let mm = gBrowser.selectedBrowser.messageManager; let promise = BrowserTestUtils.waitForMessage(mm, "Test:LoadedScripts"); - // Load a custom frame script to avoid using ContentTask which loads Task.jsm + // Load a custom frame script to avoid using SpecialPowers.spawn which may + // load other modules. mm.loadFrameScript( "data:text/javascript,(" + function () { diff --git a/browser/base/content/test/performance/browser_startup_mainthreadio.js b/browser/base/content/test/performance/browser_startup_mainthreadio.js index b65ede26d5..a89a068f13 100644 --- a/browser/base/content/test/performance/browser_startup_mainthreadio.js +++ b/browser/base/content/test/performance/browser_startup_mainthreadio.js @@ -189,6 +189,7 @@ const startupPhases = { { // bug 1541601 path: "PrfDef:channel-prefs.js", + condition: !MAC, stat: 1, read: 1, close: 1, diff --git a/browser/base/content/test/performance/browser_tabdetach.js b/browser/base/content/test/performance/browser_tabdetach.js index a860362f1f..3cbdde50fc 100644 --- a/browser/base/content/test/performance/browser_tabdetach.js +++ b/browser/base/content/test/performance/browser_tabdetach.js @@ -59,7 +59,7 @@ add_task(async function test_detach_not_overflowed() { expectedReflows: EXPECTED_REFLOWS, // we are opening a whole new window, so there's no point in tracking // rects being painted - frames: { filter: rects => [] }, + frames: { filter: () => [] }, } ); @@ -87,7 +87,7 @@ add_task(async function test_detach_overflowed() { expectedReflows: EXPECTED_REFLOWS, // we are opening a whole new window, so there's no point in tracking // rects being painted - frames: { filter: rects => [] }, + frames: { filter: () => [] }, } ); diff --git a/browser/base/content/test/performance/browser_tabopen.js b/browser/base/content/test/performance/browser_tabopen.js index 2457812cb7..b7eabf4844 100644 --- a/browser/base/content/test/performance/browser_tabopen.js +++ b/browser/base/content/test/performance/browser_tabopen.js @@ -144,7 +144,7 @@ add_task(async function () { await withPerfObserver( async function () { let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone"); - BrowserOpenTab(); + BrowserCommands.openTab(); await BrowserTestUtils.waitForEvent( gBrowser.selectedTab, "TabAnimationEnd" diff --git a/browser/base/content/test/performance/browser_tabopen_squeeze.js b/browser/base/content/test/performance/browser_tabopen_squeeze.js index f92bdc2ea4..dd73f66030 100644 --- a/browser/base/content/test/performance/browser_tabopen_squeeze.js +++ b/browser/base/content/test/performance/browser_tabopen_squeeze.js @@ -52,7 +52,7 @@ add_task(async function () { await withPerfObserver( async function () { let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone"); - BrowserOpenTab(); + BrowserCommands.openTab(); await BrowserTestUtils.waitForEvent( gBrowser.selectedTab, "TabAnimationEnd" diff --git a/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js index 1fd33ed836..50d108c062 100644 --- a/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js +++ b/browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js @@ -90,7 +90,7 @@ add_task(async function () { await withPerfObserver( async function () { let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone"); - BrowserOpenTab(); + BrowserCommands.openTab(); await BrowserTestUtils.waitForEvent( gBrowser.selectedTab, "TabAnimationEnd" @@ -115,7 +115,7 @@ add_task(async function () { await withPerfObserver( async function () { let switchDone = BrowserTestUtils.waitForEvent(window, "TabSwitchDone"); - BrowserOpenTab(); + BrowserCommands.openTab(); await switchDone; await TestUtils.waitForCondition(() => { return gBrowser.tabContainer.arrowScrollbox.hasAttribute( diff --git a/browser/base/content/test/performance/browser_tabswitch.js b/browser/base/content/test/performance/browser_tabswitch.js index bbbbac3a21..ba29efa662 100644 --- a/browser/base/content/test/performance/browser_tabswitch.js +++ b/browser/base/content/test/performance/browser_tabswitch.js @@ -59,6 +59,14 @@ add_task(async function () { getComputedStyle(gBrowser.selectedTab).paddingInlineStart ); let minTabWidth = firstTabRect.width - 2 * tabPaddingStart; + if (AppConstants.platform == "macosx") { + // On macOS, after bug 1886729, gecko screenshots like the ones for this + // test can't screenshot the native titlebar. That, plus the fact that + // there's no border or shadow (see bug 1702653) means that we only end up + // with the tab text color changing, which is smaller than the tab + // background size. + minTabWidth = 0; + } let maxTabWidth = firstTabRect.width; let inRange = (val, min, max) => min <= val && val <= max; @@ -84,11 +92,7 @@ add_task(async function () { // The tab selection changes between 2 adjacent tabs, so we expect // both to change color at once: this should be a single rect of the // width of 2 tabs. - inRange( - r.w, - minTabWidth - 1, // -1 for the border on Win7 - maxTabWidth * 2 - ) + inRange(r.w, minTabWidth, maxTabWidth * 2) ) ) ), diff --git a/browser/base/content/test/performance/browser_windowclose.js b/browser/base/content/test/performance/browser_windowclose.js index 7d11779acc..11fa669be0 100644 --- a/browser/base/content/test/performance/browser_windowclose.js +++ b/browser/base/content/test/performance/browser_windowclose.js @@ -53,7 +53,7 @@ add_task(async function () { { expectedReflows: EXPECTED_REFLOWS, frames: { - filter(rects, frame, previousFrame) { + filter(rects, frame) { // Ignore the focus-out animation. if (isLikelyFocusChange(rects, frame)) { return []; diff --git a/browser/base/content/test/performance/browser_windowopen.js b/browser/base/content/test/performance/browser_windowopen.js index 02c6172948..b258cb67f5 100644 --- a/browser/base/content/test/performance/browser_windowopen.js +++ b/browser/base/content/test/performance/browser_windowopen.js @@ -44,7 +44,7 @@ add_task(async function () { let expectations = { expectedReflows: EXPECTED_REFLOWS, frames: { - filter(rects, frame, previousFrame) { + filter(rects, frame) { // The first screenshot we get in OSX / Windows shows an unfocused browser // window for some reason. See bug 1445161. if (!alreadyFocused && isLikelyFocusChange(rects, frame)) { diff --git a/browser/base/content/test/performance/head.js b/browser/base/content/test/performance/head.js index 29722e6bbe..42f7ae95fc 100644 --- a/browser/base/content/test/performance/head.js +++ b/browser/base/content/test/performance/head.js @@ -42,7 +42,7 @@ async function recordReflows(testPromise, win = window) { let reflows = []; let observer = { - reflow(start, end) { + reflow() { // Gather information about the current code path. reflows.push(new Error().stack); @@ -50,7 +50,7 @@ async function recordReflows(testPromise, win = window) { dirtyFrame(win); }, - reflowInterruptible(start, end) { + reflowInterruptible() { // Interruptible reflows are the reflows caused by the refresh // driver ticking. These are fine. }, @@ -99,11 +99,9 @@ async function recordReflows(testPromise, win = window) { * // Sometimes, due to unpredictable timings, the reflow may be hit * // less times. * stack: [ - * "select@chrome://global/content/bindings/textbox.xml", - * "focusAndSelectUrlBar@chrome://browser/content/browser.js", - * "openLinkIn@chrome://browser/content/utilityOverlay.js", - * "openUILinkIn@chrome://browser/content/utilityOverlay.js", - * "BrowserOpenTab@chrome://browser/content/browser.js", + * "somefunction@chrome://somepackage/content/somefile.mjs", + * "otherfunction@chrome://otherpackage/content/otherfile.js", + * "morecode@resource://somewhereelse/SomeModule.sys.mjs", * ], * // We expect this particular reflow to happen up to 2 times. * maxCount: 2, @@ -113,10 +111,9 @@ async function recordReflows(testPromise, win = window) { * // This reflow is caused by lorem ipsum. We expect this reflow * // to only happen once, so we can omit the "maxCount" property. * stack: [ - * "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml", - * "_fillTrailingGap@chrome://browser/content/tabbrowser.xml", - * "_handleNewTab@chrome://browser/content/tabbrowser.xml", - * "onxbltransitionend@chrome://browser/content/tabbrowser.xml", + * "somefunction@chrome://somepackage/content/somefile.mjs", + * "otherfunction@chrome://otherpackage/content/otherfile.js", + * "morecode@resource://somewhereelse/SomeModule.sys.mjs", * ], * } * ] @@ -430,7 +427,7 @@ async function recordFrames(testPromise, win = window) { let frames = []; - let afterPaintListener = event => { + let afterPaintListener = () => { let width, height; canvas.width = width = win.innerWidth; canvas.height = height = win.innerHeight; diff --git a/browser/base/content/test/permissions/browser_autoplay_blocked.js b/browser/base/content/test/permissions/browser_autoplay_blocked.js index d81481d6a5..7fd45a4340 100644 --- a/browser/base/content/test/permissions/browser_autoplay_blocked.js +++ b/browser/base/content/test/permissions/browser_autoplay_blocked.js @@ -102,7 +102,7 @@ add_task(async function testMainViewVisible() { Services.prefs.setIntPref(AUTOPLAY_PREF, Ci.nsIAutoplay.BLOCKED); - await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function (browser) { + await BrowserTestUtils.withNewTab(AUTOPLAY_PAGE, async function () { let permissionsList = document.getElementById( "permission-popup-permission-list" ); diff --git a/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js b/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js index dbb2d1ea32..62a49e359c 100644 --- a/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js +++ b/browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js @@ -336,7 +336,7 @@ async function withNewTabInput( await SpecialPowers.spawn(browser, [], initTab); await enableResistFingerprinting(randomDataOnCanvasExtract, true); let popupShown = promisePopupShown(); - await SpecialPowers.spawn(browser, [], function (host) { + await SpecialPowers.spawn(browser, [], function () { E10SUtils.wrapHandlingUserInput(content, true, function () { var button = content.document.getElementById("clickme"); button.click(); @@ -361,11 +361,7 @@ async function withNewTabInput( await SpecialPowers.popPrefEnv(); } -async function doTestInput( - randomDataOnCanvasExtract, - grantPermission, - autoDeclineNoInput -) { +async function doTestInput(randomDataOnCanvasExtract, grantPermission) { await BrowserTestUtils.withNewTab( kUrl, withNewTabInput.bind(null, randomDataOnCanvasExtract, grantPermission) diff --git a/browser/base/content/test/permissions/browser_site_scoped_permissions.js b/browser/base/content/test/permissions/browser_site_scoped_permissions.js index 7a8953de47..949d7a0596 100644 --- a/browser/base/content/test/permissions/browser_site_scoped_permissions.js +++ b/browser/base/content/test/permissions/browser_site_scoped_permissions.js @@ -21,7 +21,7 @@ add_task(async function testSiteScopedPermissionSubdomainAffectsBaseDomain() { ); let id = "3rdPartyStorage^https://example.org"; - await BrowserTestUtils.withNewTab(EMPTY_PAGE, async function (browser) { + await BrowserTestUtils.withNewTab(EMPTY_PAGE, async function () { Services.perms.addFromPrincipal( subdomainPrincipal, id, @@ -76,49 +76,46 @@ add_task(async function testSiteScopedPermissionBaseDomainAffectsSubdomain() { Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); let id = "3rdPartyStorage^https://example.org"; - await BrowserTestUtils.withNewTab( - SUBDOMAIN_EMPTY_PAGE, - async function (browser) { - Services.perms.addFromPrincipal(principal, id, SitePermissions.ALLOW); - await openPermissionPopup(); - - let permissionsList = document.getElementById( - "permission-popup-permission-list" - ); - let listEntryCount = permissionsList.querySelectorAll( - ".permission-popup-permission-item" - ).length; - is( - listEntryCount, - 1, - "Permission exists on base domain when set on subdomain" - ); - - closePermissionPopup(); - - Services.perms.removeFromPrincipal(principal, id); - - // We intentionally turn off a11y_checks, because the following function - // is expected to click a toolbar button that may be already hidden - // with "display:none;". The permissions panel anchor is hidden because - // the last permission was removed, however we force opening the panel - // anyways in order to test that the list has been properly emptied: - AccessibilityUtils.setEnv({ - mustHaveAccessibleRule: false, - }); - await openPermissionPopup(); - AccessibilityUtils.resetEnv(); - - listEntryCount = permissionsList.querySelectorAll( - ".permission-popup-permission-item-3rdPartyStorage" - ).length; - is( - listEntryCount, - 0, - "Permission removed on base domain when removed on subdomain" - ); - - await closePermissionPopup(); - } - ); + await BrowserTestUtils.withNewTab(SUBDOMAIN_EMPTY_PAGE, async function () { + Services.perms.addFromPrincipal(principal, id, SitePermissions.ALLOW); + await openPermissionPopup(); + + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let listEntryCount = permissionsList.querySelectorAll( + ".permission-popup-permission-item" + ).length; + is( + listEntryCount, + 1, + "Permission exists on base domain when set on subdomain" + ); + + closePermissionPopup(); + + Services.perms.removeFromPrincipal(principal, id); + + // We intentionally turn off a11y_checks, because the following function + // is expected to click a toolbar button that may be already hidden + // with "display:none;". The permissions panel anchor is hidden because + // the last permission was removed, however we force opening the panel + // anyways in order to test that the list has been properly emptied: + AccessibilityUtils.setEnv({ + mustHaveAccessibleRule: false, + }); + await openPermissionPopup(); + AccessibilityUtils.resetEnv(); + + listEntryCount = permissionsList.querySelectorAll( + ".permission-popup-permission-item-3rdPartyStorage" + ).length; + is( + listEntryCount, + 0, + "Permission removed on base domain when removed on subdomain" + ); + + await closePermissionPopup(); + }); }); diff --git a/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js b/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js index 7da79b1810..490da04374 100644 --- a/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js +++ b/browser/base/content/test/permissions/browser_temporary_permissions_navigation.js @@ -41,7 +41,7 @@ add_task(async function testTempPermissionOnReload() { reloaded = BrowserTestUtils.browserLoaded(browser, false, origin); // Reload as a user (should remove the temp permission). - BrowserReload(); + BrowserCommands.reload(); await reloaded; diff --git a/browser/base/content/test/plugins/head.js b/browser/base/content/test/plugins/head.js index 4f6c25b92a..76f87dfc43 100644 --- a/browser/base/content/test/plugins/head.js +++ b/browser/base/content/test/plugins/head.js @@ -147,7 +147,7 @@ function promiseWaitForFocus(aWindow) { * @return Promise */ function waitForNotificationBar(notificationID, browser, callback) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { let notification; let notificationBox = gBrowser.getNotificationBox(browser); waitForCondition( @@ -189,7 +189,7 @@ function waitForNotificationShown(notification, callback) { } PopupNotifications.panel.addEventListener( "popupshown", - function (e) { + function () { callback(); }, { once: true } diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification.js b/browser/base/content/test/popupNotifications/browser_popupNotification.js index 235aa90b5f..4479cb1ee7 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification.js @@ -26,7 +26,7 @@ var tests = [ checkPopup(popup, this.notifyObj); triggerMainCommand(popup); }, - onHidden(popup) { + onHidden() { ok(this.notifyObj.mainActionClicked, "mainAction was clicked"); ok( !this.notifyObj.dismissalCallbackTriggered, @@ -55,7 +55,7 @@ var tests = [ checkPopup(popup, this.notifyObj); triggerSecondaryCommand(popup, 0); }, - onHidden(popup) { + onHidden() { ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked"); ok( !this.notifyObj.dismissalCallbackTriggered, @@ -89,7 +89,7 @@ var tests = [ checkPopup(popup, this.notifyObj); triggerSecondaryCommand(popup, 1); }, - onHidden(popup) { + onHidden() { ok( this.extraSecondaryActionClicked, "extra secondary action was clicked" @@ -123,7 +123,7 @@ var tests = [ checkPopup(popup, this.notifyObj); triggerSecondaryCommand(popup, 2); }, - onHidden(popup) { + onHidden() { ok( this.extraSecondaryActionClicked, "extra secondary action was clicked" @@ -145,7 +145,7 @@ var tests = [ checkPopup(popup, this.notifyObj); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { ok( this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered" @@ -205,7 +205,7 @@ var tests = [ // switch back to the old browser gBrowser.selectedTab = this.oldSelectedTab; }, - onHidden(popup) { + onHidden() { // actually remove the notification to prevent it from reappearing ok( wrongBrowserNotificationObject.dismissalCallbackTriggered, @@ -247,7 +247,7 @@ var tests = [ checkPopup(popup, this.notifyObj); this.notification2.remove(); }, - onHidden(popup) { + onHidden() { ok( !this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered" @@ -276,7 +276,7 @@ var tests = [ is(popup.children.length, 1, "only one notification left"); triggerSecondaryCommand(popup, 0); }, - onHidden(popup) { + onHidden() { ok(this.testNotif1.mainActionClicked, "main action #1 was clicked"); ok( !this.testNotif1.secondaryActionClicked, @@ -316,7 +316,7 @@ var tests = [ ); triggerMainCommand(popup); }, - onHidden(popup) { + onHidden() { ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked"); ok( !this.notifyObj.dismissalCallbackTriggered, @@ -348,7 +348,7 @@ var tests = [ ); triggerMainCommand(popup); }, - onHidden(popup) { + onHidden() { ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked"); ok( !this.notifyObj.dismissalCallbackTriggered, @@ -380,7 +380,7 @@ var tests = [ ); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { // Remove the notifications this.firstNotification.remove(); this.secondNotification.remove(); diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_2.js b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js index 8738a3b605..1b0dea66f6 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification_2.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_2.js @@ -24,7 +24,7 @@ var tests = [ checkPopup(popup, this.notifyObj); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { ok( this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered" @@ -52,7 +52,7 @@ var tests = [ ); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { let icon = document.getElementById("geo-notification-icon"); isnot( icon.getBoundingClientRect().width, @@ -84,7 +84,7 @@ var tests = [ }); this.notification = showNotification(this.notifyObj); }, - async onShown(popup) { + async onShown() { this.complete = false; // eslint-disable-next-line @microsoft/sdl/no-insecure-url await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/"); @@ -95,7 +95,7 @@ var tests = [ // eslint-disable-next-line @microsoft/sdl/no-insecure-url await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/"); }, - onHidden(popup) { + onHidden() { ok( this.complete, "Should only have hidden the notification after 3 page loads" @@ -122,7 +122,7 @@ var tests = [ }); this.notification = showNotification(this.notifyObj); }, - async onShown(popup) { + async onShown() { this.complete = false; // eslint-disable-next-line @microsoft/sdl/no-insecure-url await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/"); @@ -134,7 +134,7 @@ var tests = [ // eslint-disable-next-line @microsoft/sdl/no-insecure-url await promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/"); }, - onHidden(popup) { + onHidden() { ok( this.complete, "Should only have hidden the notification after the timeout was passed" @@ -172,7 +172,7 @@ var tests = [ this.complete = true; dismissNotification(popup); }, - onHidden(popup) { + onHidden() { ok( this.complete, "Should only have hidden the notification after it was dismissed" @@ -212,7 +212,7 @@ var tests = [ checkPopup(popup, this.notifyObj); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { this.notification.remove(); this.box.remove(); }, @@ -272,7 +272,7 @@ var tests = [ let notification = popup.children[0]; EventUtils.synthesizeMouseAtCenter(notification.closebutton, {}); }, - onHidden(popup) { + onHidden() { ok( this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered" @@ -302,7 +302,7 @@ var tests = [ ); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { ok( this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered" diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_3.js b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js index 1b7626c660..1d8b6b473b 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification_3.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_3.js @@ -27,7 +27,7 @@ var tests = [ checkPopup(popup, this.notifyObj); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { ok( !this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered" @@ -70,7 +70,7 @@ var tests = [ dismissNotification(popup); }, - onHidden(popup) { + onHidden() { this.notification1.remove(); ok( this.notifyObj1.removedCallbackTriggered, @@ -127,7 +127,7 @@ var tests = [ dismissNotification(popup); }, - onHidden(popup) { + onHidden() { this.notificationNew.remove(); gBrowser.removeTab(gBrowser.selectedTab); @@ -156,7 +156,7 @@ var tests = [ dismissNotification(popup); }); }, - onHidden(popup) { + onHidden() { ok( !this.notifyObj.mainActionClicked, "mainAction was not clicked because it was too soon" @@ -188,7 +188,7 @@ var tests = [ triggerMainCommand(popup); }, 500); }, - onHidden(popup) { + onHidden() { ok( this.notifyObj.mainActionClicked, "mainAction was clicked after the delay" @@ -308,7 +308,7 @@ var tests = [ }; showNotification(this.notifyObj); }, - async onShown(popup) { + async onShown() { info("Adding observer and performing navigation"); await Promise.all([ diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js index b0e8f016ef..3ea0e943a3 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js @@ -23,7 +23,7 @@ var tests = [ checkPopup(popup, this.testNotif); triggerMainCommand(popup); }, - onHidden(popup) { + onHidden() { ok(this.testNotif.mainActionClicked, "main action has been triggered"); }, }, @@ -38,7 +38,7 @@ var tests = [ checkPopup(popup, this.testNotif); triggerSecondaryCommand(popup, 0); }, - onHidden(popup) { + onHidden() { ok( this.testNotif.secondaryActionClicked, "secondary action has been triggered" @@ -83,7 +83,7 @@ var tests = [ dismissNotification(popup); }, - onHidden(popup) { + onHidden() { this.notification1.remove(); this.notification2.remove(); }, @@ -213,7 +213,7 @@ var tests = [ checkPopup(popup, this.notifyObj); triggerMainCommand(popup); }, - onHidden(popup) { + onHidden() { ok( this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered" @@ -237,7 +237,7 @@ var tests = [ checkPopup(popup, this.notifyObj); triggerSecondaryCommand(popup, 0); }, - onHidden(popup) { + onHidden() { ok( this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered" diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_5.js b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js index 0ec5de0c3a..48640b9b00 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification_5.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js @@ -62,7 +62,7 @@ var tests = [ this.notification1.remove(); this.notification2.remove(); }, - onHidden(popup) {}, + onHidden() {}, }, // The anchor icon should be shown for notifications in background windows. { @@ -116,7 +116,7 @@ var tests = [ this.complete = true; triggerSecondaryCommand(popup, 0); }, - onHidden(popup) { + onHidden() { ok( !this.complete, "Should have hidden the notification after navigation" @@ -155,7 +155,7 @@ var tests = [ this.complete = true; triggerSecondaryCommand(popup, 0); }, - onHidden(popup) { + onHidden() { ok( this.complete, "Should have hidden the notification after clicking Not Now" @@ -174,7 +174,7 @@ var tests = [ this.notifyObj.options.persistent = true; gNotification = showNotification(this.notifyObj); }, - async onShown(popup) { + async onShown() { this.oldSelectedTab = gBrowser.selectedTab; await BrowserTestUtils.openNewForegroundTab( gBrowser, @@ -182,7 +182,7 @@ var tests = [ "http://example.com/" ); }, - onHidden(popup) { + onHidden() { ok(true, "Should have hidden the notification after tab switch"); gBrowser.removeTab(gBrowser.selectedTab); gBrowser.selectedTab = this.oldSelectedTab; @@ -318,7 +318,7 @@ var tests = [ this.notification1.remove(); this.notification2.remove(); }, - onHidden(popup) {}, + onHidden() {}, }, // Test that persistent notifications are shown stacked by anchor on update { @@ -363,7 +363,7 @@ var tests = [ this.notification2.remove(); this.notification3.remove(); }, - onHidden(popup) {}, + onHidden() {}, }, // Test that on closebutton click, only the persistent notification // that contained the closebutton loses its persistent status. diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js b/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js index 4a68105e27..7e8e3f0269 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js @@ -36,7 +36,7 @@ var tests = [ // process of being hidden right now. isnot(popup.state, "hiding", "popup is not hiding"); }, - onHidden(popup) { + onHidden() { window.removeEventListener("command", commandTriggered, true); ok(buttonPressed, "button pressed"); }, diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js index 5c20751c3f..bd6fd38d3f 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js @@ -46,7 +46,7 @@ var tests = [ checkPopup(popup, this.notifyObj); EventUtils.synthesizeKey("KEY_Escape"); }, - onHidden(popup) { + onHidden() { ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked"); ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked"); ok( @@ -77,7 +77,7 @@ var tests = [ checkPopup(popup, this.notifyObj); EventUtils.synthesizeKey("KEY_Escape"); }, - onHidden(popup) { + onHidden() { ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked"); ok( !this.notifyObj.secondaryActionClicked, @@ -123,7 +123,7 @@ var tests = [ is(document.activeElement, popup.children[0].closebutton); this.notification.remove(); }, - onHidden(popup) {}, + onHidden() {}, }, // Test that you can switch between active notifications with the space key // and that the notification is focused on selection. diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js b/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js index a73e1f5948..68e782cea6 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js @@ -43,7 +43,7 @@ var tests = [ ); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { this.notification.remove(); gBrowser.removeTab(gBrowser.selectedTab); gBrowser.selectedTab = this.oldSelectedTab; @@ -85,7 +85,7 @@ var tests = [ ); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { this.notification.remove(); gBrowser.removeTab(gBrowser.selectedTab); gBrowser.selectedTab = this.oldSelectedTab; @@ -135,7 +135,7 @@ var tests = [ checkPopup(popup, this.notifyObj); dismissNotification(popup); }, - onHidden(popup) { + onHidden() { this.notification.remove(); gBrowser.removeTab(gBrowser.selectedTab); gBrowser.selectedTab = this.oldSelectedTab; diff --git a/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js b/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js index 3b027bc1ef..140c2be2fd 100644 --- a/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js +++ b/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js @@ -227,7 +227,7 @@ add_task(async function test_notificationReshowTabSwitch() { let panelShownPromise; info("Open a new tab which hides the notification panel."); - await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await BrowserTestUtils.withNewTab("https://example.com", async () => { info("Wait for panel to be hidden by tab switch."); await panelHiddenPromise; info( diff --git a/browser/base/content/test/popups/browser_popup_close_main_window.js b/browser/base/content/test/popups/browser_popup_close_main_window.js index 148e937bca..abf6c43c3f 100644 --- a/browser/base/content/test/popups/browser_popup_close_main_window.js +++ b/browser/base/content/test/popups/browser_popup_close_main_window.js @@ -37,7 +37,7 @@ add_task(async function closing_last_window_equals_quitting() { Services.obs.addObserver(obs, "browser-lastwindow-close-requested"); let newWin = await BrowserTestUtils.openNewBrowserWindow(); let closedPromise = BrowserTestUtils.windowClosed(newWin); - newWin.BrowserTryToCloseWindow(); + newWin.BrowserCommands.tryToCloseWindow(); await closedPromise; is(observed, 1, "Got a notification for closing the normal window."); Services.obs.removeObserver(obs, "browser-lastwindow-close-requested"); @@ -68,12 +68,12 @@ add_task(async function closing_last_window_equals_quitting() { }); let popupWin = await popupPromise; let closedPromise = BrowserTestUtils.windowClosed(newWin); - newWin.BrowserTryToCloseWindow(); + newWin.BrowserCommands.tryToCloseWindow(); await closedPromise; is(observed, 0, "Got no notification for closing the normal window."); closedPromise = BrowserTestUtils.windowClosed(popupWin); - popupWin.BrowserTryToCloseWindow(); + popupWin.BrowserCommands.tryToCloseWindow(); await closedPromise; is( observed, diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI.js b/browser/base/content/test/protectionsUI/browser_protectionsUI.js index 5dc6acebf7..e512f7a415 100644 --- a/browser/base/content/test/protectionsUI/browser_protectionsUI.js +++ b/browser/base/content/test/protectionsUI/browser_protectionsUI.js @@ -500,7 +500,7 @@ add_task(async function testNumberOfBlockedTrackers() { // attribute will only be set if the previous counter is zero. Instead, we // wait for the change of the text content of the counter. let updateCounterPromise = new Promise(resolve => { - let mut = new MutationObserver(mutations => { + let mut = new MutationObserver(() => { resolve(); mut.disconnect(); }); diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js index 00281ac415..1346fb94c1 100644 --- a/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js +++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js @@ -397,7 +397,7 @@ add_task(async function testCookiesSubViewAllowedHeuristic() { let popup; let windowCreated = TestUtils.topicObserved( "chrome-document-global-created", - (subject, data) => { + subject => { popup = subject; return true; } diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js index 26b131d4eb..02aa21474d 100644 --- a/browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js +++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js @@ -15,7 +15,7 @@ add_task(async function test_fetch() { await SpecialPowers.spawn(newTabBrowser, [], async function () { await content.wrappedJSObject .test_fetch() - .then(response => Assert.ok(false, "should have denied the request")) + .then(() => Assert.ok(false, "should have denied the request")) .catch(e => Assert.ok(true, `Caught exception: ${e}`)); }); await contentBlockingEvent; diff --git a/browser/base/content/test/protectionsUI/browser_protectionsUI_info_message.js b/browser/base/content/test/protectionsUI/browser_protectionsUI_info_message.js index fadfaaab98..1e07db2689 100644 --- a/browser/base/content/test/protectionsUI/browser_protectionsUI_info_message.js +++ b/browser/base/content/test/protectionsUI/browser_protectionsUI_info_message.js @@ -51,10 +51,10 @@ add_task(async function testPanelInfoMessage() { }); // Test that the info message is displayed when the panel opens - let container = document.getElementById("messaging-system-message-container"); + let container = document.getElementById("info-message-container"); let message = document.getElementById("protections-popup-message"); let learnMoreLink = document.querySelector( - "#messaging-system-message-container .text-link" + "#info-message-container .text-link" ); // Check the visibility of the info message. diff --git a/browser/base/content/test/referrer/head.js b/browser/base/content/test/referrer/head.js index c812d73e80..34a5f2a58e 100644 --- a/browser/base/content/test/referrer/head.js +++ b/browser/base/content/test/referrer/head.js @@ -165,7 +165,7 @@ function delayedStartupFinished(aWindow) { * @return {Promise} * @resolves With the tab once it's loaded. */ -function someTabLoaded(aWindow) { +function someTabLoaded() { return BrowserTestUtils.waitForNewTab(gTestWindow.gBrowser, null, true); } diff --git a/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js b/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js index ada8286437..ff6badb535 100644 --- a/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js +++ b/browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js @@ -15,10 +15,10 @@ function checkDataForAboutURL() { {} ); let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1); - request.onupgradeneeded = function (e) { + request.onupgradeneeded = function () { data = false; }; - request.onsuccess = function (e) { + request.onsuccess = function () { resolve(data); }; }); diff --git a/browser/base/content/test/sanitize/browser_sanitize-timespans.js b/browser/base/content/test/sanitize/browser_sanitize-timespans.js index f9be12775b..28b528d71f 100644 --- a/browser/base/content/test/sanitize/browser_sanitize-timespans.js +++ b/browser/base/content/test/sanitize/browser_sanitize-timespans.js @@ -20,7 +20,7 @@ function promiseFormHistoryRemoved() { function promiseDownloadRemoved(list) { return new Promise(resolve => { let view = { - onDownloadRemoved(download) { + onDownloadRemoved() { list.removeView(view); resolve(); }, diff --git a/browser/base/content/test/sanitize/browser_sanitize-timespans_v2.js b/browser/base/content/test/sanitize/browser_sanitize-timespans_v2.js index c732262a1a..067a651890 100644 --- a/browser/base/content/test/sanitize/browser_sanitize-timespans_v2.js +++ b/browser/base/content/test/sanitize/browser_sanitize-timespans_v2.js @@ -23,7 +23,7 @@ function promiseFormHistoryRemoved() { function promiseDownloadRemoved(list) { return new Promise(resolve => { let view = { - onDownloadRemoved(download) { + onDownloadRemoved() { list.removeView(view); resolve(); }, diff --git a/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js index 8ae0263c82..ecdc5490d4 100644 --- a/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js +++ b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js @@ -844,14 +844,14 @@ add_task(async function testLoadtimeTelemetry() { let loadTimeDistribution = Glean.privacySanitize.loadTime.testGetValue(); let expectedNumberOfCounts = Object.entries(EXPECTED_CONTEXT_COUNTS).reduce( - (acc, [key, value]) => acc + value, + (acc, [, value]) => acc + value, 0 ); // No guarantees from timers means no guarantees on buckets. // But we can guarantee it's only two samples. is( Object.entries(loadTimeDistribution.values).reduce( - (acc, [bucket, count]) => acc + count, + (acc, [, count]) => acc + count, 0 ), expectedNumberOfCounts, diff --git a/browser/base/content/test/sanitize/browser_sanitizeDialog_v2_dataSizes.js b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2_dataSizes.js index ccb3c7d519..736df32e81 100644 --- a/browser/base/content/test/sanitize/browser_sanitizeDialog_v2_dataSizes.js +++ b/browser/base/content/test/sanitize/browser_sanitizeDialog_v2_dataSizes.js @@ -10,6 +10,14 @@ ChromeUtils.defineESModuleGetters(this, { Sanitizer: "resource:///modules/Sanitizer.sys.mjs", }); +const LARGE_USAGE_NUM = 100000000000000000000000000000000000000000000000000; + +function isIframeOverflowing(win) { + return ( + win.scrollWidth > win.clientWidth || win.scrollHeight > win.clientHeight + ); +} + add_setup(async function () { await blankSlate(); registerCleanupFunction(async function () { @@ -275,7 +283,7 @@ add_task(async function testClearingBeforeDataSizesLoad() { info("stub called"); info("This promise should never resolve"); - await new Promise(resolve => {}); + await new Promise(() => {}); }); dh.onload = async function () { // we don't need to initiate a event listener to wait for the resolver to be assigned for this @@ -308,3 +316,58 @@ add_task(async function testClearingBeforeDataSizesLoad() { // Restore the sandbox after the test is complete sandbox.restore(); }); + +// tests the dialog resizing upon wrapping of text +// so that the clear buttons do not get cut out of the dialog. +add_task(async function testPossibleWrappingOfDialog() { + await blankSlate(); + + let dh = new ClearHistoryDialogHelper({ + checkingDataSizes: true, + }); + // Create a sandbox for isolated stubbing within the test + let sandbox = sinon.createSandbox(); + sandbox + .stub(SiteDataManager, "getQuotaUsageForTimeRanges") + .callsFake(async () => { + info("stubbed getQuotaUsageForTimeRanges called"); + + return { + TIMESPAN_HOUR: 0, + TIMESPAN_2HOURS: 0, + TIMESPAN_4HOURS: LARGE_USAGE_NUM, + TIMESPAN_TODAY: 0, + TIMESPAN_EVERYTHING: 0, + }; + }); + + dh.onload = async function () { + let windowObj = + window.browsingContext.topChromeWindow.gDialogBox._dialog._frame + .contentWindow; + let promise = new Promise(resolve => { + windowObj.addEventListener("resize", resolve); + }); + this.selectDuration(Sanitizer.TIMESPAN_4HOURS); + + await promise; + ok( + !isIframeOverflowing(windowObj.document.getElementById("SanitizeDialog")), + "There should be no overflow on wrapping in the dialog" + ); + + this.selectDuration(Sanitizer.TIMESPAN_2HOURS); + await promise; + ok( + !isIframeOverflowing(windowObj.document.getElementById("SanitizeDialog")), + "There should be no overflow on wrapping in the dialog" + ); + + this.cancelDialog(); + }; + dh.open(); + await dh.promiseClosed; + + // Restore the sandbox after the test is complete + sandbox.restore(); +}); diff --git a/browser/base/content/test/sanitize/head.js b/browser/base/content/test/sanitize/head.js index 30d96c69f6..1b41226fd1 100644 --- a/browser/base/content/test/sanitize/head.js +++ b/browser/base/content/test/sanitize/head.js @@ -49,10 +49,10 @@ function checkIndexedDB(host, originAttributes) { originAttributes ); let request = indexedDB.openForPrincipal(principal, "TestDatabase", 1); - request.onupgradeneeded = function (e) { + request.onupgradeneeded = function () { data = false; }; - request.onsuccess = function (e) { + request.onsuccess = function () { resolve(data); }; }); diff --git a/browser/base/content/test/sidebar/browser_sidebar_move.js b/browser/base/content/test/sidebar/browser_sidebar_move.js index 3de26b7966..d434b3bbd8 100644 --- a/browser/base/content/test/sidebar/browser_sidebar_move.js +++ b/browser/base/content/test/sidebar/browser_sidebar_move.js @@ -4,15 +4,17 @@ registerCleanupFunction(() => { }); const EXPECTED_START_ORDINALS = [ - ["sidebar-box", 1], - ["sidebar-splitter", 2], - ["appcontent", 3], + ["sidebar-launcher", 1], + ["sidebar-box", 2], + ["sidebar-splitter", 3], + ["appcontent", 4], ]; const EXPECTED_END_ORDINALS = [ - ["sidebar-box", 3], - ["sidebar-splitter", 2], - ["appcontent", 1], + ["sidebar-launcher", 5], + ["sidebar-box", 4], + ["sidebar-splitter", 3], + ["appcontent", 2], ]; function getBrowserChildrenWithOrdinals() { diff --git a/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js b/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js index 858cd3d632..79b6d216c5 100644 --- a/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js +++ b/browser/base/content/test/siteIdentity/browser_identityBlock_focus.js @@ -98,7 +98,7 @@ add_task(async function testWithNotifications() { // Checks that with invalid pageproxystate the identity block is ignored. add_task(async function testInvalidPageProxyState() { await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] }); - await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + await BrowserTestUtils.withNewTab("about:blank", async function () { // Loading about:blank will automatically focus the urlbar, which, however, can // race with the test code. So we only send the shortcut if the urlbar isn't focused yet. if (document.activeElement != gURLBar.inputField) { diff --git a/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js index 0107814b98..ff77b42ed8 100644 --- a/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js +++ b/browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js @@ -67,7 +67,7 @@ async function testClearing( }); } - await BrowserTestUtils.withNewTab(testURI, async function (browser) { + await BrowserTestUtils.withNewTab(testURI, async function () { // Verify we have added quota storage. if (testQuota) { let usage = await SiteDataTestUtils.getQuotaUsage(originA); diff --git a/browser/base/content/test/siteIdentity/browser_navigation_failures.js b/browser/base/content/test/siteIdentity/browser_navigation_failures.js index ac3fcc4067..f71552cdcf 100644 --- a/browser/base/content/test/siteIdentity/browser_navigation_failures.js +++ b/browser/base/content/test/siteIdentity/browser_navigation_failures.js @@ -85,10 +85,10 @@ function startServer(cert) { output = transport.openOutputStream(0, 0, 0); }, - onHandshakeDone(socket, status) { + onHandshakeDone() { input.asyncWait( { - onInputStreamReady(readyInput) { + onInputStreamReady() { try { input.close(); output.close(); diff --git a/browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js b/browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js index 9dce76266a..bd004bae1b 100644 --- a/browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js +++ b/browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js @@ -19,7 +19,7 @@ const NOT_SECURE_LABEL = Services.prefs.getBoolPref( * @param {string} uri - URI of the page to test with. */ async function testPageInfoNotEncrypted(uri) { - let pageInfo = BrowserPageInfo(uri, "securityTab"); + let pageInfo = BrowserCommands.pageInfo(uri, "securityTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); let pageInfoDoc = pageInfo.document; let securityTab = pageInfoDoc.getElementById("securityTab"); @@ -87,7 +87,7 @@ function startServer(cert) { output = transport.openOutputStream(0, 0, 0); }, - onHandshakeDone(socket, status) { + onHandshakeDone() { input.asyncWait( { onInputStreamReady(readyInput) { @@ -152,7 +152,7 @@ add_task(async function () { QueryInterface: ChromeUtils.generateQI(["nsISystemProxySettings"]), mainThreadOnly: true, PACURI: null, - getProxyForURI: (aSpec, aScheme, aHost, aPort) => { + getProxyForURI: () => { return `HTTPS localhost:${server.port}`; }, }; @@ -181,7 +181,7 @@ add_task(async function () { // secure, in a real situation the connection from the proxy to // http://example.com won't be secure, so we treat it as not secure. // eslint-disable-next-line @microsoft/sdl/no-insecure-url - await BrowserTestUtils.withNewTab("http://example.com/", async browser => { + await BrowserTestUtils.withNewTab("http://example.com/", async () => { let identityMode = window.document.getElementById("identity-box").className; is( identityMode, diff --git a/browser/base/content/test/siteIdentity/head.js b/browser/base/content/test/siteIdentity/head.js index 733796ffb7..9936a8bf6f 100644 --- a/browser/base/content/test/siteIdentity/head.js +++ b/browser/base/content/test/siteIdentity/head.js @@ -244,12 +244,12 @@ async function assertMixedContentBlockingState(tabbrowser, states = {}) { ); gIdentityHandler._identityIconBox.click(); await promisePanelOpen; - let popupAttr = doc - .getElementById("identity-popup") - .getAttribute("mixedcontent"); - let bodyAttr = doc - .getElementById("identity-popup-securityView-extended-info") - .getAttribute("mixedcontent"); + let popupAttr = + doc.getElementById("identity-popup").getAttribute("mixedcontent") || ""; + let bodyAttr = + doc + .getElementById("identity-popup-securityView-extended-info") + .getAttribute("mixedcontent") || ""; is( popupAttr.includes("active-loaded"), diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js index 5e83443ec7..d73c3fe8de 100644 --- a/browser/base/content/test/static/browser_all_files_referenced.js +++ b/browser/base/content/test/static/browser_all_files_referenced.js @@ -81,6 +81,9 @@ var gExceptionPaths = [ // CSS files are referenced inside JS in an html template "chrome://browser/content/aboutlogins/components/", + + // Strip on Share parameter lists + "chrome://global/content/antitracking/", ]; // These are not part of the omni.ja file, so we find them only when running @@ -99,13 +102,6 @@ if (AppConstants.MOZ_BACKGROUNDTASKS) { gExceptionPaths.push("resource://app/modules/backgroundtasks/"); } -if (AppConstants.NIGHTLY_BUILD) { - // This is nightly-only debug tool. - gExceptionPaths.push( - "chrome://browser/content/places/interactionsViewer.html" - ); -} - // Each allowlist entry should have a comment indicating which file is // referencing the listed file in a way that the test can't detect, or a // bug number to remove or use the file if it is indeed currently unreferenced. @@ -283,6 +279,9 @@ var allowlist = [ // Bug 1875361 { file: "chrome://global/content/ml/SummarizerModel.sys.mjs" }, + // Bug 1886130 + { file: "chrome://global/content/ml/ModelHub.sys.mjs" }, + // toolkit/xre/MacRunFromDmgUtils.mm { file: "resource://gre/localization/en-US/toolkit/global/run-from-dmg.ftl" }, @@ -293,6 +292,19 @@ var allowlist = [ { file: "chrome://browser/content/screenshots/download-white.svg" }, ]; +if (AppConstants.NIGHTLY_BUILD) { + allowlist.push( + ...[ + // This is nightly-only debug tool. + { file: "chrome://browser/content/places/interactionsViewer.html" }, + + // A debug tool that is only available in Nightly builds, and is accessed + // directly by developers via the chrome URI (bug 1888491) + { file: "chrome://browser/content/backup/debug.html" }, + ] + ); +} + if (AppConstants.platform != "win") { // toolkit/mozapps/defaultagent/Notification.cpp // toolkit/mozapps/defaultagent/ScheduledTask.cpp diff --git a/browser/base/content/test/static/browser_parsable_css.js b/browser/base/content/test/static/browser_parsable_css.js index 602cc5a7e2..fb91da578a 100644 --- a/browser/base/content/test/static/browser_parsable_css.js +++ b/browser/base/content/test/static/browser_parsable_css.js @@ -14,12 +14,6 @@ let ignoreList = [ { sourceName: /codemirror\.css$/i, isFromDevTools: true }, // UA-only media features. { - sourceName: /\b(autocomplete-item)\.css$/, - errorMessage: /Expected media feature name but found \u2018-moz.*/i, - isFromDevTools: false, - platforms: ["windows"], - }, - { sourceName: /\b(contenteditable|EditorOverride|svg|forms|html|mathml|ua)\.css$/i, errorMessage: /Unknown pseudo-class.*-moz-/i, @@ -27,7 +21,7 @@ let ignoreList = [ }, { sourceName: - /\b(scrollbars|xul|html|mathml|ua|forms|svg|manageDialog|autocomplete-item-shared|formautofill)\.css$/i, + /\b(scrollbars|xul|html|mathml|ua|forms|svg|manageDialog|formautofill)\.css$/i, errorMessage: /Unknown property.*-moz-/i, isFromDevTools: false, }, @@ -123,6 +117,8 @@ let propNameAllowlist = [ isFromDevTools: false, }, { propName: "--browser-stack-z-index-rdm-toolbar", isFromDevTools: false }, + // about:profiling is in devtools even though it uses non-devtools styles. + { propName: "--in-content-border-hover", isFromDevTools: false }, // These variables are specified from devtools but read from non-devtools // styles, which confuses the test. @@ -434,13 +430,13 @@ add_task(async function checkAllTheCSS() { let loadCSS = chromeUri => new Promise(resolve => { let linkEl, onLoad, onError; - onLoad = e => { + onLoad = () => { processCSSRules(linkEl.sheet); resolve(); linkEl.removeEventListener("load", onLoad); linkEl.removeEventListener("error", onError); }; - onError = e => { + onError = () => { ok( false, "Loading " + linkEl.getAttribute("href") + " threw an error!" diff --git a/browser/base/content/test/static/browser_parsable_script.js b/browser/base/content/test/static/browser_parsable_script.js index d4dcbd87fe..3d8fbc1535 100644 --- a/browser/base/content/test/static/browser_parsable_script.js +++ b/browser/base/content/test/static/browser_parsable_script.js @@ -22,9 +22,9 @@ const kESModuleList = new Set([ /chrome\/pdfjs\/content\/web\/.*\.js$/, ]); -// Normally we would use reflect.jsm to get Reflect.parse. However, if -// we do that, then all the AST data is allocated in reflect.jsm's -// zone. That exposes a bug in our GC. The GC collects reflect.jsm's +// Normally we would use reflect.sys.mjs to get Reflect.parse. However, if +// we do that, then all the AST data is allocated in reflect.sys.mjs's +// zone. That exposes a bug in our GC. The GC collects reflect.sys.mjs's // zone but not the zone in which our test code lives (since no new // data is being allocated in it). The cross-compartment wrappers in // our zone that point to the AST data never get collected, and so the @@ -69,7 +69,7 @@ function uriIsESModule(uri) { } function parsePromise(uri, parseTarget) { - let promise = new Promise((resolve, reject) => { + let promise = new Promise(resolve => { let xhr = new XMLHttpRequest(); xhr.open("GET", uri, true); xhr.onreadystatechange = function () { diff --git a/browser/base/content/test/static/head.js b/browser/base/content/test/static/head.js index d9b978e853..317ad430af 100644 --- a/browser/base/content/test/static/head.js +++ b/browser/base/content/test/static/head.js @@ -135,7 +135,7 @@ function* generateEntriesFromJarFile(jarFile, extension) { } function fetchFile(uri) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { let xhr = new XMLHttpRequest(); xhr.responseType = "text"; xhr.open("GET", uri, true); diff --git a/browser/base/content/test/sync/browser_contextmenu_sendtab.js b/browser/base/content/test/sync/browser_contextmenu_sendtab.js index 4922869c1d..bc49680e37 100644 --- a/browser/base/content/test/sync/browser_contextmenu_sendtab.js +++ b/browser/base/content/test/sync/browser_contextmenu_sendtab.js @@ -323,7 +323,7 @@ async function openTabContextMenu(openSubmenuId = null) { function promiseObserver(topic) { return new Promise(resolve => { - let obs = (aSubject, aTopic, aData) => { + let obs = (aSubject, aTopic) => { Services.obs.removeObserver(obs, aTopic); resolve(aSubject); }; diff --git a/browser/base/content/test/sync/browser_fxa_web_channel.js b/browser/base/content/test/sync/browser_fxa_web_channel.js index c232f26f26..23c5510422 100644 --- a/browser/base/content/test/sync/browser_fxa_web_channel.js +++ b/browser/base/content/test/sync/browser_fxa_web_channel.js @@ -27,7 +27,7 @@ var gTests = [ content_uri: TEST_HTTP_PATH, channel_id: TEST_CHANNEL_ID, }); - let promiseObserver = new Promise((resolve, reject) => { + let promiseObserver = new Promise(resolve => { makeObserver( ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) { @@ -52,7 +52,7 @@ var gTests = [ { desc: "fxa web channel - login messages should notify the fxAccounts object", async run() { - let promiseLogin = new Promise((resolve, reject) => { + let promiseLogin = new Promise(resolve => { let login = accountData => { Assert.equal(typeof accountData.authAt, "number"); Assert.equal(accountData.email, "testuser@testuser.com"); @@ -91,7 +91,7 @@ var gTests = [ async run() { let properUrl = TEST_BASE_URL + "?can_link_account"; - let promiseEcho = new Promise((resolve, reject) => { + let promiseEcho = new Promise(resolve => { let webChannelOrigin = Services.io.newURI(properUrl); // responses sent to content are echoed back over the // `fxaccounts_webchannel_response_echo` channel. Ensure the @@ -100,7 +100,7 @@ var gTests = [ "fxaccounts_webchannel_response_echo", webChannelOrigin ); - echoWebChannel.listen((webChannelId, message, target) => { + echoWebChannel.listen((webChannelId, message) => { Assert.equal(message.command, "fxaccounts:can_link_account"); Assert.equal(message.messageId, 2); Assert.equal(message.data.ok, true); @@ -136,7 +136,7 @@ var gTests = [ { desc: "fxa web channel - logout messages should notify the fxAccounts object", async run() { - let promiseLogout = new Promise((resolve, reject) => { + let promiseLogout = new Promise(resolve => { let logout = uid => { Assert.equal(uid, "uid"); @@ -167,7 +167,7 @@ var gTests = [ { desc: "fxa web channel - delete messages should notify the fxAccounts object", async run() { - let promiseDelete = new Promise((resolve, reject) => { + let promiseDelete = new Promise(resolve => { let logout = uid => { Assert.equal(uid, "uid"); @@ -199,8 +199,8 @@ var gTests = [ desc: "fxa web channel - firefox_view messages should call the openFirefoxView helper", async run() { let wasCalled = false; - let promiseMessageHandled = new Promise((resolve, reject) => { - let openFirefoxView = (browser, entryPoint) => { + let promiseMessageHandled = new Promise(resolve => { + let openFirefoxView = browser => { wasCalled = true; Assert.ok( !!browser.ownerGlobal, diff --git a/browser/base/content/test/sync/browser_sync.js b/browser/base/content/test/sync/browser_sync.js index 168c6f22b0..8f687842a4 100644 --- a/browser/base/content/test/sync/browser_sync.js +++ b/browser/base/content/test/sync/browser_sync.js @@ -772,7 +772,7 @@ function checkSyncNowButtons(syncing, tooltip = null) { for (const syncButton of syncButtons) { is( syncButton.getAttribute("syncstatus"), - syncing ? "active" : "", + syncing ? "active" : null, "button active has the right value" ); if (tooltip) { @@ -894,7 +894,7 @@ function checkItemsVisibilities(itemsIds, expectedShownItemId) { function promiseObserver(topic) { return new Promise(resolve => { - let obs = (aSubject, aTopic, aData) => { + let obs = (aSubject, aTopic) => { Services.obs.removeObserver(obs, aTopic); resolve(aSubject); }; diff --git a/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js b/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js index cb3a1f72d6..d6d2434017 100644 --- a/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js +++ b/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js @@ -8,14 +8,10 @@ const { PromiseTestUtils } = ChromeUtils.importESModule( ); /** - * Check that if we're using a window-modal prompt, - * the next synchronous window-internal modal prompt aborts rather than - * leaving us in a deadlock about how to enter modal state. + * Check that the next synchronous window-internal modal prompt aborts rather + * than leaving us in a deadlock about how to enter modal state. */ add_task(async function test_check_multiple_prompts() { - await SpecialPowers.pushPrefEnv({ - set: [["prompts.windowPromptSubDialog", true]], - }); let container = document.getElementById("window-modal-dialog"); let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); diff --git a/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js b/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js index 86d7c992c5..1211694973 100644 --- a/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js +++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js @@ -119,7 +119,7 @@ async function waitForDialog(doConfirmPrompt, crossDomain, prefEnabled) { } else { Assert.equal( dialog._overlay.getAttribute("hideContent"), - "", + null, "Dialog overlay does not hide the current sites content" ); Assert.equal( @@ -137,7 +137,7 @@ async function waitForDialog(doConfirmPrompt, crossDomain, prefEnabled) { } else { Assert.equal( dialog._overlay.getAttribute("hideContent"), - "", + null, "Dialog overlay does not hide the current sites content" ); Assert.equal( diff --git a/browser/base/content/test/tabPrompts/browser_contentOrigins.js b/browser/base/content/test/tabPrompts/browser_contentOrigins.js index 2bf4ba6039..0c40763a99 100644 --- a/browser/base/content/test/tabPrompts/browser_contentOrigins.js +++ b/browser/base/content/test/tabPrompts/browser_contentOrigins.js @@ -127,12 +127,6 @@ async function checkDialog( }); } -add_setup(async function () { - await SpecialPowers.pushPrefEnv({ - set: [["prompts.modalType.httpAuth", Ci.nsIPrompt.MODAL_TYPE_TAB]], - }); -}); - add_task(async function test_check_prompt_origin_display() { await checkAlert("https://example.com/", { value: "example.com" }); // eslint-disable-next-line @microsoft/sdl/no-insecure-url diff --git a/browser/base/content/test/tabPrompts/browser_windowPrompt.js b/browser/base/content/test/tabPrompts/browser_windowPrompt.js index 535142f485..0aca489b50 100644 --- a/browser/base/content/test/tabPrompts/browser_windowPrompt.js +++ b/browser/base/content/test/tabPrompts/browser_windowPrompt.js @@ -7,9 +7,6 @@ * Check that the in-window modal dialogs work correctly. */ add_task(async function test_check_window_modal_prompt_service() { - await SpecialPowers.pushPrefEnv({ - set: [["prompts.windowPromptSubDialog", true]], - }); let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); // Avoid blocking the test on the (sync) alert by sticking it in a timeout: setTimeout( @@ -69,9 +66,6 @@ add_task(async function test_check_window_modal_prompt_service() { * Check that the dialog's own closing methods being invoked don't break things. */ add_task(async function test_check_window_modal_prompt_service() { - await SpecialPowers.pushPrefEnv({ - set: [["prompts.windowPromptSubDialog", true]], - }); let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); // Avoid blocking the test on the (sync) alert by sticking it in a timeout: setTimeout( @@ -105,9 +99,6 @@ add_task(async function test_check_window_modal_prompt_service() { }); add_task(async function test_check_multiple_prompts() { - await SpecialPowers.pushPrefEnv({ - set: [["prompts.windowPromptSubDialog", true]], - }); let container = document.getElementById("window-modal-dialog"); let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); @@ -173,9 +164,6 @@ add_task(async function test_check_minimize_response() { if (AppConstants.platform == "linux") { return; } - await SpecialPowers.pushPrefEnv({ - set: [["prompts.windowPromptSubDialog", true]], - }); let promiseSizeModeChange = BrowserTestUtils.waitForEvent( window, @@ -235,10 +223,6 @@ add_task(async function test_check_minimize_response() { * underlying SubDialog has fully opened. */ add_task(async function test_closed_callback() { - await SpecialPowers.pushPrefEnv({ - set: [["prompts.windowPromptSubDialog", true]], - }); - let promptClosedPromise = Services.prompt.asyncAlert( window.browsingContext, Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, diff --git a/browser/base/content/test/tabcrashed/browser_aboutRestartRequired.toml b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired.toml index 88988434fb..8f8648d810 100644 --- a/browser/base/content/test/tabcrashed/browser_aboutRestartRequired.toml +++ b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired.toml @@ -14,8 +14,10 @@ prefs = [ #["browser_aboutRestartRequired_buildid_false-positive.js"] #skip-if = ["win11_2009 && msix && debug"] # bug 1823581 -["browser_aboutRestartRequired_buildid_mismatch.js"] -skip-if = ["win11_2009 && msix && debug"] # bug 1823581 +# Bug 1888355: re-enable once bug 1877361 is fixed +#["browser_aboutRestartRequired_buildid_mismatch.js"] +#skip-if = ["win11_2009 && msix && debug"] # bug 1823581 -["browser_aboutRestartRequired_buildid_no-platform-ini.js"] -skip-if = ["win11_2009 && msix && debug"] # bug 1823581 +# Bug 1888355: re-enable once bug 1877361 is fixed +#["browser_aboutRestartRequired_buildid_no-platform-ini.js"] +#skip-if = ["win11_2009 && msix && debug"] # bug 1823581 diff --git a/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_noForkServer.toml b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_noForkServer.toml index ec045ddf79..ddb49a0e26 100644 --- a/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_noForkServer.toml +++ b/browser/base/content/test/tabcrashed/browser_aboutRestartRequired_noForkServer.toml @@ -12,3 +12,11 @@ prefs = [ # Bug 1876056: remove once bug 1877361 is fixed ["browser_aboutRestartRequired_buildid_false-positive.js"] skip-if = ["win11_2009 && msix && debug"] # bug 1823581 + +# Bug 1888355: re-enable once bug 1877361 is fixed +["browser_aboutRestartRequired_buildid_mismatch.js"] +skip-if = ["win11_2009 && msix && debug"] # bug 1823581 + +# Bug 1888355: re-enable once bug 1877361 is fixed +["browser_aboutRestartRequired_buildid_no-platform-ini.js"] +skip-if = ["win11_2009 && msix && debug"] # bug 1823581 diff --git a/browser/base/content/test/tabcrashed/head.js b/browser/base/content/test/tabcrashed/head.js index bb57c85d6d..b4e9137012 100644 --- a/browser/base/content/test/tabcrashed/head.js +++ b/browser/base/content/test/tabcrashed/head.js @@ -117,7 +117,7 @@ async function setupLocalCrashReportServer() { // reports. This test needs them enabled. The test also needs a mock // report server, and fortunately one is already set up by toolkit/ // crashreporter/test/Makefile.in. Assign its URL to MOZ_CRASHREPORTER_URL, - // which CrashSubmit.jsm uses as a server override. + // which CrashSubmit.sys.mjs uses as a server override. let noReport = Services.env.get("MOZ_CRASHREPORTER_NO_REPORT"); let serverUrl = Services.env.get("MOZ_CRASHREPORTER_URL"); Services.env.set("MOZ_CRASHREPORTER_NO_REPORT", ""); @@ -135,7 +135,7 @@ async function setupLocalCrashReportServer() { */ function prepareNoDump() { let originalGetDumpID = TabCrashHandler.getDumpID; - TabCrashHandler.getDumpID = function (browser) { + TabCrashHandler.getDumpID = function () { return null; }; registerCleanupFunction(() => { @@ -156,11 +156,11 @@ function unsetBuildidMatchDontSendEnv() { } function getEventPromise(eventName, eventKind) { - return new Promise(function (resolve, reject) { + return new Promise(function (resolve) { info("Installing event listener (" + eventKind + ")"); window.addEventListener( eventName, - event => { + () => { ok(true, "Received " + eventName + " (" + eventKind + ") event"); info("Call resolve() for " + eventKind + " event"); resolve(); diff --git a/browser/base/content/test/tabs/browser.toml b/browser/base/content/test/tabs/browser.toml index 1b4a6900bf..fa77a8b1a4 100644 --- a/browser/base/content/test/tabs/browser.toml +++ b/browser/base/content/test/tabs/browser.toml @@ -76,6 +76,8 @@ support-files = ["tab_that_closes.html"] ["browser_hiddentab_contextmenu.js"] +["browser_lastSeenActive.js"] + ["browser_lazy_tab_browser_events.js"] ["browser_link_in_tab_title_and_url_prefilled_blank_page.js"] diff --git a/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js b/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js index b782c3aada..fea1de8fe0 100644 --- a/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js +++ b/browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js @@ -13,7 +13,7 @@ add_task(async function () { }); let promiseTab = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI_SOURCE); - BrowserViewSource(tab.linkedBrowser); + BrowserCommands.viewSource(tab.linkedBrowser); let viewSourceTab = await promiseTab; registerCleanupFunction(async function () { BrowserTestUtils.removeTab(viewSourceTab); diff --git a/browser/base/content/test/tabs/browser_audioTabIcon.js b/browser/base/content/test/tabs/browser_audioTabIcon.js index 53b5140abb..c065e2b173 100644 --- a/browser/base/content/test/tabs/browser_audioTabIcon.js +++ b/browser/base/content/test/tabs/browser_audioTabIcon.js @@ -396,7 +396,7 @@ async function test_swapped_browser_while_not_playing(oldTab, newBrowser) { ); let AudioPlaybackPromise = new Promise(resolve => { - let observer = (subject, topic, data) => { + let observer = () => { ok(false, "Should not see an audio-playback notification"); }; Services.obs.addObserver(observer, "audio-playback"); @@ -443,7 +443,7 @@ async function test_swapped_browser_while_not_playing(oldTab, newBrowser) { await test_tooltip(newTab.overlayIcon, "Unmute tab", true, newTab); } -async function test_browser_swapping(tab, browser) { +async function test_browser_swapping(tab) { // First, test swapping with a playing but muted tab. await play(tab); diff --git a/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js b/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js index 4610551977..8fbee64db4 100644 --- a/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js +++ b/browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js @@ -86,7 +86,7 @@ add_task(async function test_principal_ctrl_click() { await BrowserTestUtils.withNewTab( "about:test-about-principal-parent", - async function (browser) { + async function () { let loadPromise = BrowserTestUtils.waitForNewTab( gBrowser, "about:test-about-principal-child", @@ -149,7 +149,7 @@ add_task(async function test_principal_right_click_open_link_in_new_tab() { await BrowserTestUtils.withNewTab( "about:test-about-principal-parent", - async function (browser) { + async function () { let loadPromise = BrowserTestUtils.waitForNewTab( gBrowser, "about:test-about-principal-child", diff --git a/browser/base/content/test/tabs/browser_e10s_about_process.js b/browser/base/content/test/tabs/browser_e10s_about_process.js index f73e8e659c..504dfe0265 100644 --- a/browser/base/content/test/tabs/browser_e10s_about_process.js +++ b/browser/base/content/test/tabs/browser_e10s_about_process.js @@ -37,7 +37,7 @@ const TEST_MODULES = [ function AboutModule() {} AboutModule.prototype = { - newChannel(aURI, aLoadInfo) { + newChannel() { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); }, @@ -52,7 +52,7 @@ AboutModule.prototype = { return 0; }, - getIndexedDBOriginPostfix(aURI) { + getIndexedDBOriginPostfix() { return null; }, diff --git a/browser/base/content/test/tabs/browser_lastSeenActive.js b/browser/base/content/test/tabs/browser_lastSeenActive.js new file mode 100644 index 0000000000..d6bba57d93 --- /dev/null +++ b/browser/base/content/test/tabs/browser_lastSeenActive.js @@ -0,0 +1,260 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { SessionStoreTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SessionStoreTestUtils.sys.mjs" +); + +const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL; + +SessionStoreTestUtils.init(this, window); +// take a state snapshot we can restore to after each test +const ORIG_STATE = SessionStore.getBrowserState(); + +const SECOND_MS = 1000; +const DAY_MS = 24 * 60 * 60 * 1000; +const today = new Date().getTime(); +const yesterday = new Date(Date.now() - DAY_MS).getTime(); + +function tabEntry(url, lastAccessed) { + return { + entries: [{ url, triggeringPrincipal_base64 }], + lastAccessed, + }; +} + +/** + * Make the given window focused and active + */ +async function switchToWindow(win) { + info("switchToWindow, waiting for promiseFocus"); + await SimpleTest.promiseFocus(win); + info("switchToWindow, waiting for correct Services.focus.activeWindow"); + await BrowserTestUtils.waitForCondition( + () => Services.focus.activeWindow == win + ); +} + +async function changeSizeMode(win, mode) { + let promise = BrowserTestUtils.waitForEvent(win, "sizemodechange"); + win[mode](); + await promise; +} + +async function cleanup() { + await switchToWindow(window); + await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE); + is( + BrowserWindowTracker.orderedWindows.length, + 1, + "One window at the end of test cleanup" + ); + info("cleanup, browser state restored"); +} + +function deltaTime(time, expectedTime) { + return Math.abs(expectedTime - time); +} + +function getWindowUrl(win) { + return win.gBrowser.selectedBrowser?.currentURI?.spec; +} + +function getWindowByTabUrl(url) { + return BrowserWindowTracker.orderedWindows.find( + win => getWindowUrl(win) == url + ); +} + +add_task(async function restoredTabs() { + const now = Date.now(); + await SessionStoreTestUtils.promiseBrowserState({ + windows: [ + { + tabs: [ + tabEntry("data:,Window0-Tab0", yesterday), + tabEntry("data:,Window0-Tab1", yesterday), + ], + selected: 2, + }, + ], + }); + is( + gBrowser.visibleTabs[1], + gBrowser.selectedTab, + "The selected tab is the 2nd visible tab" + ); + is( + getWindowUrl(window), + "data:,Window0-Tab1", + "The expected tab is selected" + ); + Assert.greaterOrEqual( + gBrowser.selectedTab.lastSeenActive, + now, + "The selected tab's lastSeenActive is now" + ); + Assert.greaterOrEqual( + gBrowser.selectedTab.lastAccessed, + now, + "The selected tab's lastAccessed is now" + ); + + // tab restored from session but never seen or active + is( + gBrowser.visibleTabs[0].lastSeenActive, + yesterday, + "The restored tab's lastSeenActive is yesterday" + ); + await cleanup(); +}); + +add_task(async function switchingTabs() { + let now = Date.now(); + let initialTab = gBrowser.selectedTab; + let applicationStart = Services.startup.getStartupInfo().start.getTime(); + let openedTab = BrowserTestUtils.addTab(gBrowser, "data:,Tab1"); + await BrowserTestUtils.browserLoaded(openedTab.linkedBrowser); + + ok(!openedTab.selected, "The background tab we opened isn't selected"); + Assert.greaterOrEqual( + initialTab.selected && initialTab.lastSeenActive, + now, + "The initial tab is selected and last seen now" + ); + + is( + openedTab.lastSeenActive, + applicationStart, + `Background tab got default lastSeenActive value, delta: ${deltaTime( + openedTab.lastSeenActive, + applicationStart + )}` + ); + + now = Date.now(); + await BrowserTestUtils.switchTab(gBrowser, openedTab); + Assert.greaterOrEqual( + openedTab.lastSeenActive, + now, + "The tab we switched to is last seen now" + ); + + await cleanup(); +}); + +add_task(async function switchingWindows() { + info("Restoring to the test browser state"); + await SessionStoreTestUtils.promiseBrowserState({ + windows: [ + { + tabs: [tabEntry("data:,Window1-Tab0", yesterday)], + selected: 1, + sizemodeBeforeMinimized: "normal", + sizemode: "maximized", + zIndex: 1, // this will be the selected window + }, + { + tabs: [tabEntry("data:,Window2-Tab0", yesterday)], + selected: 1, + sizemodeBeforeMinimized: "normal", + sizemode: "maximized", + zIndex: 2, + }, + ], + }); + info("promiseBrowserState resolved"); + info( + `BrowserWindowTracker.pendingWindows: ${BrowserWindowTracker.pendingWindows.size}` + ); + await Promise.all( + Array.from(BrowserWindowTracker.pendingWindows.values()).map( + win => win.deferred.promise + ) + ); + info("All the pending windows are resolved"); + info("Waiting for the firstBrowserLoaded in each of the windows"); + await Promise.all( + BrowserWindowTracker.orderedWindows.map(win => { + const selectedUrl = getWindowUrl(win); + if (selectedUrl && selectedUrl !== "about:blank") { + return Promise.resolve(); + } + return BrowserTestUtils.firstBrowserLoaded(win, false); + }) + ); + let expectedTabURLs = ["data:,Window1-Tab0", "data:,Window2-Tab0"]; + let [win1, win2] = expectedTabURLs.map(url => getWindowByTabUrl(url)); + if (BrowserWindowTracker.getTopWindow() !== win1) { + info("Switch to win1 which isn't active/top after restoring session"); + // In theory the zIndex values in the session state should make win1 active + // But in practice that isn't always true. To ensure we're testing from a known state, + // ensure the first window is active before proceeding with the test + await switchToWindow(win1); + [win1, win2] = expectedTabURLs.map(url => getWindowByTabUrl(url)); + } + + let actualTabURLs = Array.from(BrowserWindowTracker.orderedWindows).map(win => + getWindowUrl(win) + ); + Assert.deepEqual( + actualTabURLs, + expectedTabURLs, + "Both windows are open with selected tab URLs in the expected order" + ); + + let lastSeenTimes = [win1, win2].map( + win => win.gBrowser.selectedTab.lastSeenActive + ); + + info("Focusing the other window"); + await switchToWindow(win2); + // wait a little so the timestamps will differ and then check again + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(res => setTimeout(res, 100)); + Assert.greater( + win2.gBrowser.selectedTab.lastSeenActive, + lastSeenTimes[1], + "The foreground window selected tab is last seen more recently than it was before being focused" + ); + Assert.greater( + win2.gBrowser.selectedTab.lastSeenActive, + win1.gBrowser.selectedTab.lastSeenActive, + "The foreground window selected tab is last seen more recently than the backgrounded one" + ); + + lastSeenTimes = [win1, win2].map( + win => win.gBrowser.selectedTab.lastSeenActive + ); + // minimize the foreground window and focus the other + let promiseSizeModeChange = BrowserTestUtils.waitForEvent( + win2, + "sizemodechange" + ); + win2.minimize(); + info("Waiting for the sizemodechange on minimized window"); + await promiseSizeModeChange; + await switchToWindow(win1); + + ok( + !win2.gBrowser.selectedTab.linkedBrowser.docShellIsActive, + "Docshell should be Inactive" + ); + ok(win2.document.hidden, "Minimized windows's document should be hidden"); + + // wait a little so the timestamps will differ and then check again + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(res => setTimeout(res, 100)); + Assert.greater( + win1.gBrowser.selectedTab.lastSeenActive, + win2.gBrowser.selectedTab.lastSeenActive, + "The foreground window selected tab is last seen more recently than the minimized one" + ); + Assert.greater( + win1.gBrowser.selectedTab.lastSeenActive, + lastSeenTimes[0], + "The foreground window selected tab is last seen more recently than it was before being focused" + ); + + await cleanup(); +}); diff --git a/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js b/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js index 665bdb7f69..5e09225cde 100644 --- a/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js +++ b/browser/base/content/test/tabs/browser_lazy_tab_browser_events.js @@ -93,10 +93,10 @@ add_task(async function test_hidden_muted_lazy_tabs_and_swapping() { mutedTab.toggleMuteAudio(); gBrowser.hideTab(hiddenTab); - is(lazyTab.linkedPanel, "", "lazyTab is lazy"); - is(hiddenTab.linkedPanel, "", "hiddenTab is lazy"); - is(mutedTab.linkedPanel, "", "mutedTab is lazy"); - is(normalTab.linkedPanel, "", "normalTab is lazy"); + is(lazyTab.linkedPanel, null, "lazyTab is lazy"); + is(hiddenTab.linkedPanel, null, "hiddenTab is lazy"); + is(mutedTab.linkedPanel, null, "mutedTab is lazy"); + is(normalTab.linkedPanel, null, "normalTab is lazy"); ok(mutedTab.linkedBrowser.audioMuted, "mutedTab is muted"); ok(hiddenTab.hidden, "hiddenTab is hidden"); @@ -117,7 +117,7 @@ add_task(async function test_hidden_muted_lazy_tabs_and_swapping() { }); gBrowser.swapBrowsersAndCloseOther(lazyTab, mutedTab); tabEventTracker.checkExpectations(); - is(lazyTab.linkedPanel, "", "muted lazyTab is still lazy"); + is(lazyTab.linkedPanel, null, "muted lazyTab is still lazy"); ok(lazyTab.linkedBrowser.audioMuted, "muted lazyTab is now muted"); ok(!lazyTab.hidden, "muted lazyTab is not hidden"); @@ -133,7 +133,7 @@ add_task(async function test_hidden_muted_lazy_tabs_and_swapping() { }); gBrowser.swapBrowsersAndCloseOther(lazyTab, hiddenTab); tabEventTracker.checkExpectations(); - is(lazyTab.linkedPanel, "", "hidden lazyTab is still lazy"); + is(lazyTab.linkedPanel, null, "hidden lazyTab is still lazy"); ok(!lazyTab.linkedBrowser.audioMuted, "hidden lazyTab is not muted any more"); ok(lazyTab.hidden, "hidden lazyTab has been hidden"); @@ -149,7 +149,7 @@ add_task(async function test_hidden_muted_lazy_tabs_and_swapping() { }); gBrowser.swapBrowsersAndCloseOther(lazyTab, normalTab); tabEventTracker.checkExpectations(); - is(lazyTab.linkedPanel, "", "normal lazyTab is still lazy"); + is(lazyTab.linkedPanel, null, "normal lazyTab is still lazy"); ok(!lazyTab.linkedBrowser.audioMuted, "normal lazyTab is not muted any more"); ok(!lazyTab.hidden, "normal lazyTab is not hidden any more"); diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js index f8773e3720..9212667a35 100644 --- a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js @@ -66,7 +66,7 @@ add_task(async function normal_page__foreground__abort() { tab: WAIT_A_BIT_LOADING_TITLE, urlbar: UrlbarTestUtils.trimURL(WAIT_A_BIT_URL), }, - async actionWhileLoading(onTabLoaded) { + async actionWhileLoading() { info("Abort loading"); document.getElementById("stop-button").click(); }, @@ -160,7 +160,7 @@ add_task(async function normal_page__background__abort() { tab: WAIT_A_BIT_LOADING_TITLE, urlbar: UrlbarTestUtils.trimURL(HOME_URL), }, - async actionWhileLoading(onTabLoaded) { + async actionWhileLoading() { info("Abort loading"); document.getElementById("stop-button").click(); }, diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js index 07cf7a8ea2..57e28ca834 100644 --- a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js @@ -44,7 +44,7 @@ add_task(async function normal_page__by_script__abort() { tab: BLANK_TITLE, urlbar: UrlbarTestUtils.trimURL(BLANK_URL), }, - async actionWhileLoading(onTabLoaded) { + async actionWhileLoading() { info("Abort loading"); document.getElementById("stop-button").click(); }, diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js index ab18d7c7e0..464a7c43de 100644 --- a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js @@ -47,7 +47,7 @@ add_task(async function normal_page__no_target__abort() { tab: HOME_TITLE, urlbar: UrlbarTestUtils.trimURL(HOME_URL), }, - async actionWhileLoading(onTabLoaded) { + async actionWhileLoading() { info("Abort loading"); document.getElementById("stop-button").click(); }, diff --git a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js index 7dc0e8fa45..53242ca359 100644 --- a/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js +++ b/browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js @@ -45,7 +45,7 @@ add_task(async function normal_page__other_target__foreground__abort() { tab: BLANK_TITLE, urlbar: UrlbarTestUtils.trimURL(BLANK_URL), }, - async actionWhileLoading(onTabLoaded) { + async actionWhileLoading() { info("Abort loading"); document.getElementById("stop-button").click(); }, @@ -117,7 +117,7 @@ add_task(async function normal_page__other_target__background__abort() { tab: WAIT_A_BIT_LOADING_TITLE, urlbar: UrlbarTestUtils.trimURL(HOME_URL), }, - async actionWhileLoading(onTabLoaded) { + async actionWhileLoading() { info("Abort loading"); document.getElementById("stop-button").click(); }, diff --git a/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js b/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js index db0571a2c0..89952b6c4d 100644 --- a/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js +++ b/browser/base/content/test/tabs/browser_long_data_url_label_truncation.js @@ -33,7 +33,7 @@ add_task(async function test_ensure_truncation() { let fileReader = new FileReader(); const DATA_URL = await new Promise(resolve => { - fileReader.addEventListener("load", e => resolve(fileReader.result)); + fileReader.addEventListener("load", () => resolve(fileReader.result)); fileReader.readAsDataURL(new Blob([MOBY], { type: "text/html" })); }); // Substring the full URL to avoid log clutter because Assert will print diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js index d668d21df8..f294769898 100644 --- a/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js @@ -61,9 +61,9 @@ add_task(async function testLazyTabs() { await triggerClickOn(oldTabs[i], { ctrlKey: true }); } - isnot(oldTabs[0].linkedPanel, "", `Old tab 0 shouldn't be lazy`); + isnot(oldTabs[0].linkedPanel, null, `Old tab 0 shouldn't be lazy`); for (let i = 1; i < numTabs; ++i) { - is(oldTabs[i].linkedPanel, "", `Old tab ${i} should be lazy`); + is(oldTabs[i].linkedPanel, null, `Old tab ${i} should be lazy`); } is(gBrowser.multiSelectedTabsCount, numTabs, `${numTabs} multiselected tabs`); @@ -79,11 +79,11 @@ add_task(async function testLazyTabs() { if (i == 0) { isnot( oldTab.linkedPanel, - "", + null, `Old tab ${i} should continue not being lazy` ); } else if (i > 0) { - is(oldTab.linkedPanel, "", `Old tab ${i} should continue being lazy`); + is(oldTab.linkedPanel, null, `Old tab ${i} should continue being lazy`); } else { return; } @@ -101,9 +101,13 @@ add_task(async function testLazyTabs() { await tabsMoved; let newTabs = newWindow.gBrowser.tabs; - isnot(newTabs[0].linkedPanel, "", `New tab 0 should continue not being lazy`); + isnot( + newTabs[0].linkedPanel, + null, + `New tab 0 should continue not being lazy` + ); for (let i = 1; i < numTabs; ++i) { - is(newTabs[i].linkedPanel, "", `New tab ${i} should continue being lazy`); + is(newTabs[i].linkedPanel, null, `New tab ${i} should continue being lazy`); } is( diff --git a/browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js b/browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js index 66258659fd..157254142d 100644 --- a/browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js +++ b/browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js @@ -12,7 +12,7 @@ async function expectHeightChanges(tab, expectedNewHeightChanges, msg) { let contentObservedHeightChanges = await ContentTask.spawn( tab.linkedBrowser, null, - async args => { + async () => { await new Promise(resolve => content.requestAnimationFrame(resolve)); return content.document.body.innerText; } @@ -109,7 +109,7 @@ add_task(async function () { info("Opening a new tab, making the previous tab non-selected"); await expectBmToolbarVisibilityChange( () => { - BrowserOpenTab(); + BrowserCommands.openTab(); ok( !tab.selected, "non-new tab is in the background (not the selected tab)" diff --git a/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js b/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js index ec11951cb0..568510b20a 100644 --- a/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js +++ b/browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js @@ -159,7 +159,7 @@ add_task(async function process_switching_through_navigation_features() { assertIsPrivilegedProcess(browser, "new tab opened from about:newtab"); // Check that reload does not break the privileged about: content process affinity. - BrowserReload(); + BrowserCommands.reload(); await BrowserTestUtils.browserLoaded(browser, false, ABOUT_NEWTAB); assertIsPrivilegedProcess(browser, "about:newtab after reload"); diff --git a/browser/base/content/test/tabs/browser_new_tab_url.js b/browser/base/content/test/tabs/browser_new_tab_url.js index 233cb4e59e..ab00560929 100644 --- a/browser/base/content/test/tabs/browser_new_tab_url.js +++ b/browser/base/content/test/tabs/browser_new_tab_url.js @@ -3,7 +3,7 @@ "use strict"; add_task(async function test_browser_open_newtab_default_url() { - BrowserOpenTab(); + BrowserCommands.openTab(); const tab = gBrowser.selectedTab; if (tab.linkedBrowser.currentURI.spec !== window.BROWSER_NEW_TAB_URL) { @@ -19,7 +19,7 @@ add_task(async function test_browser_open_newtab_default_url() { add_task(async function test_browser_open_newtab_specific_url() { const url = "https://example.com"; - BrowserOpenTab({ url }); + BrowserCommands.openTab({ url }); const tab = gBrowser.selectedTab; await BrowserTestUtils.browserLoaded(tab.linkedBrowser); diff --git a/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js b/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js index cb9fc3c6d7..2bc26cf667 100644 --- a/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js +++ b/browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js @@ -9,10 +9,10 @@ add_task(async function test_browser_open_newtab_start_observer_notification() { Services.obs.addObserver(observe, "browser-open-newtab-start"); }); - // We're calling BrowserOpenTab() (rather the using BrowserTestUtils + // We're calling BrowserCommands.openTab() (rather the using BrowserTestUtils // because we want to be sure that it triggers the event to fire, since // it's very close to where various user-actions are triggered. - BrowserOpenTab(); + BrowserCommands.openTab(); const newTabCreatedPromise = await observerFiredPromise; const browser = await newTabCreatedPromise; const tab = gBrowser.selectedTab; diff --git a/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js b/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js index fbcd0bb492..4631afba42 100644 --- a/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js +++ b/browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js @@ -5,14 +5,14 @@ function test() { waitForExplicitFinish(); - function testState(aPinned) { + function testState() { function elemAttr(id, attr) { return document.getElementById(id).getAttribute(attr); } is( elemAttr("key_close", "disabled"), - "", + null, "key_closed should always be enabled" ); is( diff --git a/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js b/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js index 9e1c1ff5cd..922f94b07c 100644 --- a/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js +++ b/browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js @@ -140,7 +140,7 @@ add_task(async function process_switching_through_navigation_features() { ); // Check that reload does not break the privileged mozilla content process affinity. - BrowserReload(); + BrowserCommands.reload(); await BrowserTestUtils.browserLoaded(browser, false, TEST_HIGH1); is( browser.frameLoader.remoteTab.osPid, diff --git a/browser/base/content/test/tabs/browser_removeTabs_order.js b/browser/base/content/test/tabs/browser_removeTabs_order.js index 071cc03716..a993415653 100644 --- a/browser/base/content/test/tabs/browser_removeTabs_order.js +++ b/browser/base/content/test/tabs/browser_removeTabs_order.js @@ -16,7 +16,7 @@ add_task(async function () { // Add a beforeunload event listener in one of the tabs; it should be called // before closing any of the tabs. await ContentTask.spawn(tab2.linkedBrowser, null, async function () { - content.window.addEventListener("beforeunload", function (event) {}, true); + content.window.addEventListener("beforeunload", function () {}, true); }); let permitUnloadSpy = sinon.spy(tab2.linkedBrowser, "asyncPermitUnload"); diff --git a/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js b/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js index dae4ffc444..59cfd37c0d 100644 --- a/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js +++ b/browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js @@ -11,7 +11,7 @@ add_task(async function test_pip_label_changes_tab() { let pipLabel = pipTab.querySelector(".tab-icon-sound-pip-label"); - await BrowserTestUtils.withNewTab("about:blank", async browser => { + await BrowserTestUtils.withNewTab("about:blank", async () => { let selectedTab = newWin.document.querySelector( ".tabbrowser-tab[selected]" ); diff --git a/browser/base/content/test/tabs/browser_tab_manager_visibility.js b/browser/base/content/test/tabs/browser_tab_manager_visibility.js index b7de777512..df6e75cd66 100644 --- a/browser/base/content/test/tabs/browser_tab_manager_visibility.js +++ b/browser/base/content/test/tabs/browser_tab_manager_visibility.js @@ -17,7 +17,7 @@ add_task(async function tab_manager_visibility_preference_on() { gBrowser: newWindow.gBrowser, url: TEST_HOSTNAME + DUMMY_PAGE_PATH, }, - async function (browser) { + async function () { await Assert.ok( BrowserTestUtils.isVisible( newWindow.document.getElementById("alltabs-button") @@ -39,7 +39,7 @@ add_task(async function tab_manager_visibility_preference_off() { gBrowser: newWindow.gBrowser, url: TEST_HOSTNAME + DUMMY_PAGE_PATH, }, - async function (browser) { + async function () { await Assert.ok( BrowserTestUtils.isHidden( newWindow.document.getElementById("alltabs-button") diff --git a/browser/base/content/test/tabs/browser_tab_preview.js b/browser/base/content/test/tabs/browser_tab_preview.js index 718afbb940..0f83b1e28c 100644 --- a/browser/base/content/test/tabs/browser_tab_preview.js +++ b/browser/base/content/test/tabs/browser_tab_preview.js @@ -4,14 +4,14 @@ "use strict"; +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + async function openPreview(tab) { - const previewShown = BrowserTestUtils.waitForEvent( - document.getElementById("tabbrowser-tab-preview"), - "previewshown", - false, - e => { - return e.detail.tab === tab; - } + const previewShown = BrowserTestUtils.waitForPopupEvent( + document.getElementById("tab-preview-panel"), + "shown" ); EventUtils.synthesizeMouseAtCenter(tab, { type: "mouseover" }); return previewShown; @@ -19,9 +19,9 @@ async function openPreview(tab) { async function closePreviews() { const tabs = document.getElementById("tabbrowser-tabs"); - const previewHidden = BrowserTestUtils.waitForEvent( - document.getElementById("tabbrowser-tab-preview"), - "previewhidden" + const previewHidden = BrowserTestUtils.waitForPopupEvent( + document.getElementById("tab-preview-panel"), + "hidden" ); EventUtils.synthesizeMouse(tabs, 0, tabs.outerHeight + 1, { type: "mouseout", @@ -53,35 +53,24 @@ add_task(async function hoverTests() { const tabUrl2 = "data:text/html,<html><head><title>Second New Tab</title></head><body>Hello</body></html>"; const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2); - const previewContainer = document.getElementById("tabbrowser-tab-preview"); + const previewContainer = document.getElementById("tab-preview-panel"); await openPreview(tab1); - Assert.ok( - ["open", "showing"].includes(previewContainer.panel.state), - "tab1 preview shown" - ); Assert.equal( - previewContainer.renderRoot.querySelector(".tab-preview-title").innerText, + previewContainer.querySelector(".tab-preview-title").innerText, "First New Tab", "Preview of tab1 shows correct title" ); + await closePreviews(); await openPreview(tab2); - Assert.ok( - ["open", "showing"].includes(previewContainer.panel.state), - "tab2 preview shown" - ); Assert.equal( - previewContainer.renderRoot.querySelector(".tab-preview-title").innerText, + previewContainer.querySelector(".tab-preview-title").innerText, "Second New Tab", "Preview of tab2 shows correct title" ); await closePreviews(); - Assert.ok( - ["closed", "hiding"].includes(previewContainer.panel.state), - "preview container is now hidden" - ); BrowserTestUtils.removeTab(tab1); BrowserTestUtils.removeTab(tab2); @@ -105,29 +94,41 @@ add_task(async function thumbnailTests() { const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1); const tabUrl2 = "about:blank"; const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2); - const previewContainer = document.getElementById("tabbrowser-tab-preview"); + const previewPanel = document.getElementById("tab-preview-panel"); - const thumbnailUpdated = BrowserTestUtils.waitForEvent( - previewContainer, - "previewThumbnailUpdated" + let thumbnailUpdated = BrowserTestUtils.waitForEvent( + previewPanel, + "previewThumbnailUpdated", + false, + evt => evt.detail.thumbnail ); await openPreview(tab1); await thumbnailUpdated; Assert.ok( - previewContainer.renderRoot.querySelectorAll("img,canvas").length, + previewPanel.querySelectorAll( + ".tab-preview-thumbnail-container img, .tab-preview-thumbnail-container canvas" + ).length, "Tab1 preview contains thumbnail" ); + await closePreviews(); + thumbnailUpdated = BrowserTestUtils.waitForEvent( + previewPanel, + "previewThumbnailUpdated" + ); await openPreview(tab2); + await thumbnailUpdated; Assert.equal( - previewContainer.renderRoot.querySelectorAll("img,canvas").length, + previewPanel.querySelectorAll( + ".tab-preview-thumbnail-container img, .tab-preview-thumbnail-container canvas" + ).length, 0, "Tab2 (selected) does not contain thumbnail" ); - const previewHidden = BrowserTestUtils.waitForEvent( - document.getElementById("tabbrowser-tab-preview"), - "previewhidden" + const previewHidden = BrowserTestUtils.waitForPopupEvent( + previewPanel, + "hidden" ); BrowserTestUtils.removeTab(tab1); @@ -144,6 +145,102 @@ add_task(async function thumbnailTests() { }); /** + * make sure delay is applied when mouse leaves tabstrip + * but not when moving between tabs on the tabstrip + */ +add_task(async function delayTests() { + const tabUrl1 = + "data:text/html,<html><head><title>First New Tab</title></head><body>Hello</body></html>"; + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1); + const tabUrl2 = + "data:text/html,<html><head><title>Second New Tab</title></head><body>Hello</body></html>"; + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2); + const previewComponent = gBrowser.tabContainer.previewPanel; + const previewElement = document.getElementById("tab-preview-panel"); + + sinon.spy(previewComponent, "deactivate"); + + await openPreview(tab1); + + // I can't fake this like in hoverTests, need to send an updated-tab signal + //await openPreview(tab2); + + const previewHidden = BrowserTestUtils.waitForPopupEvent( + previewElement, + "hidden" + ); + Assert.ok( + !previewComponent.deactivate.called, + "Delay is not reset when moving between tabs" + ); + + EventUtils.synthesizeMouseAtCenter(document.getElementById("reload-button"), { + type: "mousemove", + }); + + await previewHidden; + + Assert.ok( + previewComponent.deactivate.called, + "Delay is reset when cursor leaves tabstrip" + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + sinon.restore(); +}); + +/** + * Dragging a tab should deactivate the preview + */ +add_task(async function dragTests() { + await SpecialPowers.pushPrefEnv({ + set: [["ui.tooltip.delay_ms", 1000]], + }); + const tabUrl1 = + "data:text/html,<html><head><title>First New Tab</title></head><body>Hello</body></html>"; + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1); + const tabUrl2 = + "data:text/html,<html><head><title>Second New Tab</title></head><body>Hello</body></html>"; + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2); + const previewComponent = gBrowser.tabContainer.previewPanel; + const previewElement = document.getElementById("tab-preview-panel"); + + sinon.spy(previewComponent, "deactivate"); + + await openPreview(tab1); + const previewHidden = BrowserTestUtils.waitForPopupEvent( + previewElement, + "hidden" + ); + let dragend = BrowserTestUtils.waitForEvent(tab1, "dragend"); + EventUtils.synthesizePlainDragAndDrop({ + srcElement: tab1, + destElement: tab2, + }); + + await previewHidden; + + Assert.ok( + previewComponent.deactivate.called, + "delay is reset after drag started" + ); + + await dragend; + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + sinon.restore(); + + // Move the mouse outside of the tab strip. + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mouseover", + }); + + await SpecialPowers.popPrefEnv(); +}); + +/** * Wheel events at the document-level of the window should hide the preview. */ add_task(async function wheelTests() { @@ -155,9 +252,9 @@ add_task(async function wheelTests() { await openPreview(tab1); const tabs = document.getElementById("tabbrowser-tabs"); - const previewHidden = BrowserTestUtils.waitForEvent( - document.getElementById("tabbrowser-tab-preview"), - "previewhidden" + const previewHidden = BrowserTestUtils.waitForPopupEvent( + document.getElementById("tab-preview-panel"), + "hidden" ); // Copied from apz_test_native_event_utils.js diff --git a/browser/base/content/test/tabs/browser_tab_tooltips.js b/browser/base/content/test/tabs/browser_tab_tooltips.js index ee82816bce..79be4d0a36 100644 --- a/browser/base/content/test/tabs/browser_tab_tooltips.js +++ b/browser/base/content/test/tabs/browser_tab_tooltips.js @@ -57,7 +57,7 @@ add_task(async function () { ); is( tooltip.getAttribute("position"), - "", + null, "tooltip position attribute for tab" ); diff --git a/browser/base/content/test/tabs/browser_tabswitch_select.js b/browser/base/content/test/tabs/browser_tabswitch_select.js index 3868764bed..b22a75c79c 100644 --- a/browser/base/content/test/tabs/browser_tabswitch_select.js +++ b/browser/base/content/test/tabs/browser_tabswitch_select.js @@ -35,7 +35,7 @@ add_task(async function () { let fullScreenEntered = TestUtils.waitForCondition( () => document.documentElement.getAttribute("sizemode") == "fullscreen" ); - BrowserFullScreen(); + BrowserCommands.fullScreen(); await fullScreenEntered; tab2.linkedBrowser.focus(); @@ -54,7 +54,7 @@ add_task(async function () { let fullScreenExited = TestUtils.waitForCondition( () => document.documentElement.getAttribute("sizemode") != "fullscreen" ); - BrowserFullScreen(); + BrowserCommands.fullScreen(); await fullScreenExited; BrowserTestUtils.removeTab(gBrowser.selectedTab); diff --git a/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js b/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js index b5d2762eec..82f9eb871b 100644 --- a/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js +++ b/browser/base/content/test/tabs/browser_tabswitch_updatecommands.js @@ -8,7 +8,7 @@ add_task(async function () { let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, uri); let updates = []; - function countUpdates(event) { + function countUpdates() { updates.push(new Error().stack); } let updater = document.getElementById("editMenuCommandSetAll"); diff --git a/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js b/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js index b5ae94ce84..2c301a400d 100644 --- a/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js +++ b/browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js @@ -33,7 +33,7 @@ add_task(async function () { // Make sure we can view-source on the data URI page. let promiseTab = BrowserTestUtils.waitForNewTab(gBrowser, DATA_URI_SOURCE); - BrowserViewSource(fileBrowser); + BrowserCommands.viewSource(fileBrowser); let viewSourceTab = await promiseTab; registerCleanupFunction(async function () { BrowserTestUtils.removeTab(viewSourceTab); diff --git a/browser/base/content/test/tabs/browser_window_open_modifiers.js b/browser/base/content/test/tabs/browser_window_open_modifiers.js index b4376d6824..2ef951efef 100644 --- a/browser/base/content/test/tabs/browser_window_open_modifiers.js +++ b/browser/base/content/test/tabs/browser_window_open_modifiers.js @@ -97,7 +97,7 @@ add_task(async function () { BrowserTestUtils.synthesizeMouseAtCenter(id, { ...event }, browser); } else { // Make sure the keyboard activates a simple button on the page. - await ContentTask.spawn(browser, id, elementId => { + await ContentTask.spawn(browser, id, () => { content.document.querySelector("#focus-result").value = ""; content.document.querySelector("#focus-check").focus(); }); diff --git a/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js b/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js index a06b982615..bff14e5ced 100644 --- a/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js +++ b/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js @@ -244,7 +244,7 @@ async function synthesizeMouse(browser, link, event) { async function waitForNewTabWithLoadRequest() { return new Promise(resolve => gBrowser.addTabsProgressListener({ - onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags) { if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { gBrowser.removeTabsProgressListener(this); resolve(gBrowser.getTabForBrowser(aBrowser)); diff --git a/browser/base/content/test/webextensions/browser_extension_update_background.js b/browser/base/content/test/webextensions/browser_extension_update_background.js index 490544b2ec..5619bacb4d 100644 --- a/browser/base/content/test/webextensions/browser_extension_update_background.js +++ b/browser/base/content/test/webextensions/browser_extension_update_background.js @@ -87,7 +87,7 @@ async function backgroundUpdateTest(url, id, checkIconFn) { let addonId = addon.id; ok(addon, "Addon was installed"); - is(getBadgeStatus(), "", "Should not start out with an addon alert badge"); + is(getBadgeStatus(), null, "Should not start out with an addon alert badge"); // Trigger an update check and wait for the update for this addon // to be downloaded. @@ -156,7 +156,7 @@ async function backgroundUpdateTest(url, id, checkIconFn) { BrowserTestUtils.removeTab(tab); // Alert badge and hamburger menu items should be gone - is(getBadgeStatus(), "", "Addon alert badge should be gone"); + is(getBadgeStatus(), null, "Addon alert badge should be gone"); await gCUITestUtils.openMainMenu(); addons = PanelUI.addonNotificationContainer; @@ -205,7 +205,7 @@ async function backgroundUpdateTest(url, id, checkIconFn) { BrowserTestUtils.removeTab(tab); - is(getBadgeStatus(), "", "Addon alert badge should be gone"); + is(getBadgeStatus(), null, "Addon alert badge should be gone"); await addon.uninstall(); await SpecialPowers.popPrefEnv(); diff --git a/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js index a0b10c82e2..204e7fb44a 100644 --- a/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js +++ b/browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js @@ -81,7 +81,7 @@ async function testNoPrompt(origUrl, id) { await updatePromise; // There should be no notifications about the update - is(getBadgeStatus(), "", "Should not have addon alert badge"); + is(getBadgeStatus(), null, "Should not have addon alert badge"); await gCUITestUtils.openMainMenu(); let addons = PanelUI.addonNotificationContainer; diff --git a/browser/base/content/test/webextensions/browser_legacy_webext.xpi b/browser/base/content/test/webextensions/browser_legacy_webext.xpi Binary files differindex a3bdf6f832..afd0a8bcee 100644 --- a/browser/base/content/test/webextensions/browser_legacy_webext.xpi +++ b/browser/base/content/test/webextensions/browser_legacy_webext.xpi diff --git a/browser/base/content/test/webextensions/browser_permissions_installTrigger.js b/browser/base/content/test/webextensions/browser_permissions_installTrigger.js index a227518ebb..36b4efff8b 100644 --- a/browser/base/content/test/webextensions/browser_permissions_installTrigger.js +++ b/browser/base/content/test/webextensions/browser_permissions_installTrigger.js @@ -9,6 +9,12 @@ async function installTrigger(filename) { ["extensions.InstallTriggerImpl.enabled", true], // Relax the user input requirements while running this test. ["xpinstall.userActivation.required", false], + // This test asserts that the extension icon is in the install dialog + // and so it requires the signature checks to be enabled (otherwise the + // extension icon is expected to be replaced by a warning icon) and the + // two test extension used by this test (browser_webext_nopermissions.xpi + // and browser_webext_permissions.xpi) are signed using AMO stage signatures. + ["xpinstall.signatures.dev-root", true], ], }); BrowserTestUtils.startLoadingURIString( diff --git a/browser/base/content/test/webextensions/browser_permissions_local_file.js b/browser/base/content/test/webextensions/browser_permissions_local_file.js index 7f8f256e14..22dff8cb38 100644 --- a/browser/base/content/test/webextensions/browser_permissions_local_file.js +++ b/browser/base/content/test/webextensions/browser_permissions_local_file.js @@ -32,9 +32,22 @@ add_task(async function test_install_extension_from_local_file() { }, }); + await SpecialPowers.pushPrefEnv({ + set: [ + // This test asserts that the extension icon is in the install dialog + // and so it requires the signature checks to be enabled (otherwise the + // extension icon is expected to be replaced by a warning icon) and the + // two test extension used by this test (browser_webext_nopermissions.xpi + // and browser_webext_permissions.xpi) are signed using AMO stage signatures. + ["xpinstall.signatures.dev-root", true], + ], + }); + // Install the add-ons. await testInstallMethod(installFile, "installLocal"); + await SpecialPowers.popPrefEnv(); + // Check we got an installId. ok( firstInstallId != null && !isNaN(firstInstallId), diff --git a/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js b/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js index 55a578221d..d54038bffe 100644 --- a/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js +++ b/browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js @@ -9,6 +9,17 @@ async function installMozAM(filename) { ); await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await SpecialPowers.pushPrefEnv({ + set: [ + // This test asserts that the extension icon is in the install dialog + // and so it requires the signature checks to be enabled (otherwise the + // extension icon is expected to be replaced by a warning icon) and the + // two test extension used by this test (browser_webext_nopermissions.xpi + // and browser_webext_permissions.xpi) are signed using AMO stage signatures. + ["xpinstall.signatures.dev-root", true], + ], + }); + await SpecialPowers.spawn( gBrowser.selectedBrowser, [`${BASE}/${filename}`], @@ -16,6 +27,8 @@ async function installMozAM(filename) { await content.wrappedJSObject.installMozAM(url); } ); + + await SpecialPowers.popPrefEnv(); } add_task(() => testInstallMethod(installMozAM, "installAmo")); diff --git a/browser/base/content/test/webextensions/browser_permissions_pointerevent.js b/browser/base/content/test/webextensions/browser_permissions_pointerevent.js index 188aa8e3bf..2809ffe9b4 100644 --- a/browser/base/content/test/webextensions/browser_permissions_pointerevent.js +++ b/browser/base/content/test/webextensions/browser_permissions_pointerevent.js @@ -9,15 +9,15 @@ add_task(async function test_pointerevent() { e.preventDefault(); }); - document.addEventListener("mousedown", e => { + document.addEventListener("mousedown", () => { browser.test.assertTrue(true, "Should receive mousedown"); }); - document.addEventListener("mouseup", e => { + document.addEventListener("mouseup", () => { browser.test.assertTrue(true, "Should receive mouseup"); }); - document.addEventListener("pointerup", e => { + document.addEventListener("pointerup", () => { browser.test.assertTrue(true, "Should receive pointerup"); browser.test.sendMessage("done"); }); diff --git a/browser/base/content/test/webextensions/browser_update_checkForUpdates.js b/browser/base/content/test/webextensions/browser_update_checkForUpdates.js index b902527cae..c9e59556e1 100644 --- a/browser/base/content/test/webextensions/browser_update_checkForUpdates.js +++ b/browser/base/content/test/webextensions/browser_update_checkForUpdates.js @@ -3,7 +3,7 @@ function checkAll(win) { triggerPageOptionsAction(win, "check-for-updates"); return new Promise(resolve => { let observer = { - observe(subject, topic, data) { + observe() { Services.obs.removeObserver(observer, "EM-update-check-finished"); resolve(); }, diff --git a/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi b/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi Binary files differindex ab97d96a11..87500ceb38 100644 --- a/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi +++ b/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_permissions.xpi b/browser/base/content/test/webextensions/browser_webext_permissions.xpi Binary files differindex a8c8c38ef8..8149ce7b6b 100644 --- a/browser/base/content/test/webextensions/browser_webext_permissions.xpi +++ b/browser/base/content/test/webextensions/browser_webext_permissions.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update1.xpi b/browser/base/content/test/webextensions/browser_webext_update1.xpi Binary files differindex 086b3839b9..66ad3e1b31 100644 --- a/browser/base/content/test/webextensions/browser_webext_update1.xpi +++ b/browser/base/content/test/webextensions/browser_webext_update1.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update2.xpi b/browser/base/content/test/webextensions/browser_webext_update2.xpi Binary files differindex 19967c39c0..a120a64c6d 100644 --- a/browser/base/content/test/webextensions/browser_webext_update2.xpi +++ b/browser/base/content/test/webextensions/browser_webext_update2.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi b/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi Binary files differindex 24cb7616d2..040f8f8c97 100644 --- a/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi +++ b/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi b/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi Binary files differindex fd9cf7eb0e..0b13e7c7dd 100644 --- a/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi +++ b/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi b/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi Binary files differindex f4942f9082..60b6643a12 100644 --- a/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi +++ b/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi diff --git a/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi b/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi Binary files differindex 2c023edc9d..64c2afb473 100644 --- a/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi +++ b/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi diff --git a/browser/base/content/test/webextensions/head.js b/browser/base/content/test/webextensions/head.js index 84f7cd02d7..f1183c61b8 100644 --- a/browser/base/content/test/webextensions/head.js +++ b/browser/base/content/test/webextensions/head.js @@ -302,7 +302,7 @@ function checkNotification(panel, checkIcon, permissions, sideloaded) { * * @returns {Promise} */ -async function testInstallMethod(installFn, telemetryBase) { +async function testInstallMethod(installFn) { const PERMS_XPI = "browser_webext_permissions.xpi"; const NO_PERMS_XPI = "browser_webext_nopermissions.xpi"; const ID = "permissions@test.mozilla.org"; diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js index dd20a672c3..f1052565b8 100644 --- a/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js @@ -58,7 +58,7 @@ var gTests = [ ); is( gBrowser.selectedTab.getAttribute("sharing"), - "", + null, "the new tab doesn't have the 'sharing' attribute" ); is( @@ -89,7 +89,7 @@ var gTests = [ await TestUtils.waitForCondition(() => !tab.getAttribute("sharing")); is( tab.getAttribute("sharing"), - "", + null, "the tab no longer has the 'sharing' attribute after closing the stream" ); } diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js index 3e5ca0668a..9598bb565c 100644 --- a/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js @@ -49,7 +49,7 @@ add_task(async function test_get_user_media_by_device_id() { .filter(d => d.kind == "videoinput") .map(d => d.deviceId)[0]; - await BrowserTestUtils.withNewTab(TEST_PAGE, async browser => { + await BrowserTestUtils.withNewTab(TEST_PAGE, async () => { let promise = promisePopupNotificationShown("webRTC-shareDevices"); let observerPromise = expectObserverCalled("getUserMedia:request"); await promiseRequestDevice({ deviceId: { exact: audioId } }); diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js index 0df69bb9da..624bc07ce0 100644 --- a/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js +++ b/browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js @@ -171,7 +171,7 @@ var gTests = [ { desc: "getUserMedia camera+mic survives page reload but not past grace", - run: async function checkAudioVideoGracePastReload(browser) { + run: async function checkAudioVideoGracePastReload() { await prompt(true, true); await allow(true, true); await closeStream(); @@ -240,7 +240,7 @@ var gTests = [ info("Open same page in a new tab"); await disableObserverVerification(); - await BrowserTestUtils.withNewTab(SAME_ORIGIN + PATH, async browser => { + await BrowserTestUtils.withNewTab(SAME_ORIGIN + PATH, async () => { info("In new tab, gUM(camera+mic) causes a prompt."); await prompt(true, true); }); @@ -329,7 +329,7 @@ var gTests = [ { desc: "getUserMedia camera+mic grace period cleared on permission block", - run: async function checkAudioVideoGraceEndsNewTab(browser) { + run: async function checkAudioVideoGraceEndsNewTab() { await SpecialPowers.pushPrefEnv({ set: [["privacy.webrtc.deviceGracePeriodTimeoutMs", 10000]], }); diff --git a/browser/base/content/test/webrtc/browser_devices_select_audio_output.js b/browser/base/content/test/webrtc/browser_devices_select_audio_output.js index fbced1f5cc..df75f39a1a 100644 --- a/browser/base/content/test/webrtc/browser_devices_select_audio_output.js +++ b/browser/base/content/test/webrtc/browser_devices_select_audio_output.js @@ -220,7 +220,7 @@ var gTests = [ gBrowser.selectedBrowser.browsingContext, "getUserMedia:response:allow", 1, - (aSubject, aTopic, aData) => { + aSubject => { const device = aSubject .QueryInterface(Ci.nsIArrayExtensions) .GetElementAt(0).wrappedJSObject; diff --git a/browser/base/content/test/webrtc/browser_webrtc_hooks.js b/browser/base/content/test/webrtc/browser_webrtc_hooks.js index e980b15286..3fb2fc9f8d 100644 --- a/browser/base/content/test/webrtc/browser_webrtc_hooks.js +++ b/browser/base/content/test/webrtc/browser_webrtc_hooks.js @@ -122,7 +122,7 @@ var gTests = [ run: async function testDeferredBlocker(browser) { Events.on(); - let blocker = params => Promise.resolve("allow"); + let blocker = () => Promise.resolve("allow"); webrtcUI.addPeerConnectionBlocker(blocker); await tryPeerConnection(browser); @@ -138,7 +138,7 @@ var gTests = [ run: async function testBlockerDeny(browser) { Events.on(); - let blocker = params => "deny"; + let blocker = () => "deny"; webrtcUI.addPeerConnectionBlocker(blocker); await tryPeerConnection(browser, "NotAllowedError"); @@ -156,14 +156,14 @@ var gTests = [ Events.on(); let blocker1Called = false, - blocker1 = params => { + blocker1 = () => { blocker1Called = true; return "allow"; }; webrtcUI.addPeerConnectionBlocker(blocker1); let blocker2Called = false, - blocker2 = params => { + blocker2 = () => { blocker2Called = true; return "allow"; }; @@ -187,14 +187,14 @@ var gTests = [ Events.on(); let blocker1Called = false, - blocker1 = params => { + blocker1 = () => { blocker1Called = true; return "allow"; }; webrtcUI.addPeerConnectionBlocker(blocker1); let blocker2Called = false, - blocker2 = params => { + blocker2 = () => { blocker2Called = true; return "deny"; }; @@ -218,14 +218,14 @@ var gTests = [ Events.on(); let blocker1Called = false, - blocker1 = params => { + blocker1 = () => { blocker1Called = true; return "deny"; }; webrtcUI.addPeerConnectionBlocker(blocker1); let blocker2Called = false, - blocker2 = params => { + blocker2 = () => { blocker2Called = true; return "allow"; }; @@ -252,14 +252,14 @@ var gTests = [ Events.on(); let blocker1Called = false, - blocker1 = params => { + blocker1 = () => { blocker1Called = true; return "allow"; }; webrtcUI.addPeerConnectionBlocker(blocker1); let blocker2Called = false, - blocker2 = params => { + blocker2 = () => { blocker2Called = true; return "allow"; }; @@ -283,14 +283,14 @@ var gTests = [ run: async function testBlockerThrows(browser) { Events.on(); let blocker1Called = false, - blocker1 = params => { + blocker1 = () => { blocker1Called = true; throw new Error("kaboom"); }; webrtcUI.addPeerConnectionBlocker(blocker1); let blocker2Called = false, - blocker2 = params => { + blocker2 = () => { blocker2Called = true; return "allow"; }; @@ -313,10 +313,10 @@ var gTests = [ run: async function testBlockerCancel(browser) { let blocker, blockerPromise = new Promise(resolve => { - blocker = params => { + blocker = () => { resolve(); // defer indefinitely - return new Promise(innerResolve => {}); + return new Promise(() => {}); }; }); webrtcUI.addPeerConnectionBlocker(blocker); diff --git a/browser/base/content/test/webrtc/head.js b/browser/base/content/test/webrtc/head.js index 639ae2e51a..694875bd21 100644 --- a/browser/base/content/test/webrtc/head.js +++ b/browser/base/content/test/webrtc/head.js @@ -146,7 +146,7 @@ async function assertWebRTCIndicatorStatus(expected) { if (!expected) { let win = Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator"); if (win) { - await new Promise((resolve, reject) => { + await new Promise(resolve => { win.addEventListener("unload", function listener(e) { if (e.target == win.document) { win.removeEventListener("unload", listener); @@ -308,7 +308,7 @@ function expectObserverCalledOnClose( { topic: aTopic, count: 1, - filterFunctionSource: ((subject, topic, data) => { + filterFunctionSource: ((subject, topic) => { Services.cpmm.sendAsyncMessage("WebRTCTest:ObserverCalled", { topic, }); @@ -1061,7 +1061,7 @@ async function promiseReloadFrame(aFrameId, aBrowsingContext) { let loadedPromise = BrowserTestUtils.browserLoaded( gBrowser.selectedBrowser, true, - arg => { + () => { return true; } ); diff --git a/browser/base/content/test/zoom/browser.toml b/browser/base/content/test/zoom/browser.toml index 281fb9329c..e2aa0c077a 100644 --- a/browser/base/content/test/zoom/browser.toml +++ b/browser/base/content/test/zoom/browser.toml @@ -34,7 +34,7 @@ https_first_disabled = true ["browser_sitespecific_video_zoom.js"] https_first_disabled = true -support-files = ["../general/video.ogg"] +support-files = ["../general/video.webm"] skip-if = [ "os == 'win' && debug", # Bug 1315042 "verify && debug && os == 'linux'", # Bug 1315042 diff --git a/browser/base/content/test/zoom/browser_sitespecific_video_zoom.js b/browser/base/content/test/zoom/browser_sitespecific_video_zoom.js index 589e3d09cf..94fd0dee56 100644 --- a/browser/base/content/test/zoom/browser_sitespecific_video_zoom.js +++ b/browser/base/content/test/zoom/browser_sitespecific_video_zoom.js @@ -8,7 +8,7 @@ const TEST_PAGE = "http://example.org/browser/browser/base/content/test/zoom/zoom_test.html"; const TEST_VIDEO = // eslint-disable-next-line @microsoft/sdl/no-insecure-url - "http://example.org/browser/browser/base/content/test/general/video.ogg"; + "http://example.org/browser/browser/base/content/test/general/video.webm"; var gTab1, gTab2, gLevel1; diff --git a/browser/base/content/test/zoom/browser_zoom_commands.js b/browser/base/content/test/zoom/browser_zoom_commands.js index 88b6f42059..ef49a5794e 100644 --- a/browser/base/content/test/zoom/browser_zoom_commands.js +++ b/browser/base/content/test/zoom/browser_zoom_commands.js @@ -65,7 +65,7 @@ function assertTextZoomCommandCheckedState(isChecked) { * zoom levels and/or preferences on an individual browser. */ add_task(async function test_update_browser_zoom() { - await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async browser => { + await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async () => { let currentZoom = await FullZoomHelper.getGlobalValue(); Assert.equal( currentZoom, @@ -136,7 +136,7 @@ add_task(async function test_update_browser_zoom() { * zoom levels when the default zoom is not at 1.0. */ add_task(async function test_update_browser_zoom() { - await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async browser => { + await BrowserTestUtils.withNewTab(TEST_PAGE_URL, async () => { let currentZoom = await FullZoomHelper.getGlobalValue(); Assert.equal( currentZoom, diff --git a/browser/base/content/test/zoom/head.js b/browser/base/content/test/zoom/head.js index 4a42aed98f..272303de95 100644 --- a/browser/base/content/test/zoom/head.js +++ b/browser/base/content/test/zoom/head.js @@ -34,7 +34,7 @@ var FullZoomHelper = { parsedZoomValue, nonPrivateLoadContext, { - handleCompletion(reason) { + handleCompletion() { resolve(); }, } @@ -72,7 +72,7 @@ var FullZoomHelper = { value = parseFloat(pref.value); } }, - handleCompletion(reason) { + handleCompletion() { resolve(value); }, handleError(error) { @@ -84,7 +84,7 @@ var FullZoomHelper = { waitForLocationChange: function waitForLocationChange() { return new Promise(resolve => { - Services.obs.addObserver(function obs(subj, topic, data) { + Services.obs.addObserver(function obs(subj, topic) { Services.obs.removeObserver(obs, topic); resolve(); }, "browser-fullZoom:location-change"); @@ -124,7 +124,7 @@ var FullZoomHelper = { let didLoad = false; let didZoom = false; - promiseTabLoadEvent(tab, url).then(event => { + promiseTabLoadEvent(tab, url).then(() => { didLoad = true; if (didZoom) { resolve(); diff --git a/browser/base/content/titlebar-items.inc.xhtml b/browser/base/content/titlebar-items.inc.xhtml index 057fd522a9..4fea3a1266 100644 --- a/browser/base/content/titlebar-items.inc.xhtml +++ b/browser/base/content/titlebar-items.inc.xhtml @@ -13,7 +13,7 @@ data-l10n-id="browser-window-maximize-button" /> <toolbarbutton class="titlebar-button titlebar-restore" - oncommand="window.fullScreen ? BrowserFullScreen() : window.restore();" + oncommand="window.fullScreen ? BrowserCommands.fullScreen() : window.restore();" data-l10n-id="browser-window-restore-down-button" /> <toolbarbutton class="titlebar-button titlebar-close" diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js index 5967c878b3..dc3aeffaed 100644 --- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -294,7 +294,7 @@ function closeMenus(node) { * to check if the close command key was pressed in aEvent. */ function eventMatchesKey(aEvent, aKey) { - let keyPressed = aKey.getAttribute("key").toLowerCase(); + let keyPressed = (aKey.getAttribute("key") || "").toLowerCase(); let keyModifiers = aKey.getAttribute("modifiers"); let modifiers = ["Alt", "Control", "Meta", "Shift"]; @@ -341,7 +341,7 @@ function gatherTextUnder(root) { } else if (HTMLImageElement.isInstance(node)) { // If it has an "alt" attribute, add that. var altText = node.getAttribute("alt"); - if (altText && altText != "") { + if (altText) { text += " " + altText; } } diff --git a/browser/base/content/webext-panels.js b/browser/base/content/webext-panels.js index e8820f4ad4..787193ab7d 100644 --- a/browser/base/content/webext-panels.js +++ b/browser/base/content/webext-panels.js @@ -125,22 +125,16 @@ function getBrowser(panel) { return readyPromise.then(initBrowser); } -// Stub tabbrowser implementation for use by the tab-modal alert code. +// Stub tabbrowser implementation to make sure that links from inside +// extension sidebar panels open in new tabs, see bug 1488055. var gBrowser = { get selectedBrowser() { return document.getElementById("webext-panels-browser"); }, - getTabForBrowser(browser) { + getTabForBrowser() { return null; }, - - getTabModalPromptBox(browser) { - if (!browser.tabModalPromptBox) { - browser.tabModalPromptBox = new TabModalPromptBox(browser); - } - return browser.tabModalPromptBox; - }, }; function updatePosition() { diff --git a/browser/base/content/webrtcIndicator.js b/browser/base/content/webrtcIndicator.js index f38c7446ba..c19bfe1f35 100644 --- a/browser/base/content/webrtcIndicator.js +++ b/browser/base/content/webrtcIndicator.js @@ -47,7 +47,7 @@ function closingInternally() { * Main control object for the WebRTC global indicator */ const WebRTCIndicator = { - init(event) { + init() { addEventListener("load", this); addEventListener("unload", this); diff --git a/browser/base/jar.mn b/browser/base/jar.mn index 1342630d54..9357cf1af0 100644 --- a/browser/base/jar.mn +++ b/browser/base/jar.mn @@ -40,6 +40,7 @@ browser.jar: content/browser/browser-addons.js (content/browser-addons.js) content/browser/browser-allTabsMenu.js (content/browser-allTabsMenu.js) content/browser/browser-captivePortal.js (content/browser-captivePortal.js) + content/browser/browser-commands.js (content/browser-commands.js) content/browser/browser-ctrlTab.js (content/browser-ctrlTab.js) content/browser/browser-customization.js (content/browser-customization.js) content/browser/browser-data-submission-info-bar.js (content/browser-data-submission-info-bar.js) @@ -54,7 +55,6 @@ browser.jar: content/browser/browser-places.js (content/browser-places.js) content/browser/browser-profiles.js (content/browser-profiles.js) content/browser/browser-safebrowsing.js (content/browser-safebrowsing.js) - content/browser/browser-sidebar.js (content/browser-sidebar.js) content/browser/browser-siteIdentity.js (content/browser-siteIdentity.js) content/browser/browser-sitePermissionPanel.js (content/browser-sitePermissionPanel.js) content/browser/browser-siteProtections.js (content/browser-siteProtections.js) diff --git a/browser/components/BrowserContentHandler.sys.mjs b/browser/components/BrowserContentHandler.sys.mjs index ca7cf4d2c4..247f33c8b0 100644 --- a/browser/components/BrowserContentHandler.sys.mjs +++ b/browser/components/BrowserContentHandler.sys.mjs @@ -438,7 +438,7 @@ function openBrowserWindow( }); } -function openPreferences(cmdLine, extraArgs) { +function openPreferences(cmdLine) { openBrowserWindow(cmdLine, lazy.gSystemPrincipal, "about:preferences"); } @@ -1244,24 +1244,12 @@ nsDefaultCommandLineHandler.prototype = { async function handleNotification() { let { tagWasHandled } = await alertService.handleWindowsTag(tag); - // If the tag was not handled via callback, then the notification was - // from a prior instance of the application and we need to handle - // fallback behavior. - if (!tagWasHandled) { - console.info( - `Completing Windows notification (tag=${JSON.stringify( - tag - )}, notificationData=${notificationData})` + try { + notificationData = JSON.parse(notificationData); + } catch (e) { + console.error( + `Failed to parse (notificationData=${notificationData}) for Windows notification (tag=${tag})` ); - try { - notificationData = JSON.parse(notificationData); - } catch (e) { - console.error( - `Completing Windows notification (tag=${JSON.stringify( - tag - )}, failed to parse (notificationData=${notificationData})` - ); - } } // This is awkward: the relaunch data set by the caller is _wrapped_ @@ -1275,11 +1263,7 @@ nsDefaultCommandLineHandler.prototype = { ); } catch (e) { console.error( - `Completing Windows notification (tag=${JSON.stringify( - tag - )}, failed to parse (opaqueRelaunchData=${ - notificationData.opaqueRelaunchData - })` + `Failed to parse (opaqueRelaunchData=${notificationData.opaqueRelaunchData}) for Windows notification (tag=${tag})` ); } } @@ -1298,9 +1282,16 @@ nsDefaultCommandLineHandler.prototype = { // window to perform the action in. let winForAction; - if (notificationData?.launchUrl && !opaqueRelaunchData) { - // Unprivileged Web Notifications contain a launch URL and are handled - // slightly differently than privileged notifications with actions. + if ( + !tagWasHandled && + notificationData?.launchUrl && + !opaqueRelaunchData + ) { + // Unprivileged Web Notifications contain a launch URL and are + // handled slightly differently than privileged notifications with + // actions. If the tag was not handled, then the notification was + // from a prior instance of the application and we need to handle + // fallback behavior. let { uri, principal } = resolveURIInternal( cmdLine, notificationData.launchUrl @@ -1347,6 +1338,14 @@ nsDefaultCommandLineHandler.prototype = { }); } + // Note: at time of writing `opaqueRelaunchData` was only used by the + // Messaging System; if present it could be inferred that the message + // originated from the Messaging System. The Messaging System did not + // act on Windows 8 style notification callbacks, so there was no risk + // of duplicating behavior. If a non-Messaging System consumer is + // modified to populate `opaqueRelaunchData` or the Messaging System + // modified to use the callback directly, we will need to revisit + // this assumption. if (opaqueRelaunchData && winForAction) { // Without dispatch, `OPEN_URL` with `where: "tab"` does not work on relaunch. Services.tm.dispatchToMainThread(() => { diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs index f4ea0c87a3..52f4a77d82 100644 --- a/browser/components/BrowserGlue.sys.mjs +++ b/browser/components/BrowserGlue.sys.mjs @@ -27,9 +27,10 @@ ChromeUtils.defineESModuleGetters(lazy, { BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs", + ContentRelevancyManager: + "resource://gre/modules/ContentRelevancyManager.sys.mjs", ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.sys.mjs", - Corroborate: "resource://gre/modules/Corroborate.sys.mjs", DAPTelemetrySender: "resource://gre/modules/DAPTelemetrySender.sys.mjs", DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", Discovery: "resource:///modules/Discovery.sys.mjs", @@ -80,8 +81,7 @@ ChromeUtils.defineESModuleGetters(lazy, { Sanitizer: "resource:///modules/Sanitizer.sys.mjs", SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs", ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", - SearchSERPDomainToCategoriesMap: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + SearchSERPCategorization: "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs", SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", @@ -223,6 +223,22 @@ let JSPROCESSACTORS = { * available at https://firefox-source-docs.mozilla.org/dom/ipc/jsactors.html */ let JSWINDOWACTORS = { + Megalist: { + parent: { + esModuleURI: "resource://gre/actors/MegalistParent.sys.mjs", + }, + child: { + esModuleURI: "resource://gre/actors/MegalistChild.sys.mjs", + events: { + DOMContentLoaded: {}, + }, + }, + includeChrome: true, + matches: ["chrome://global/content/megalist/megalist.html"], + allFrames: true, + enablePreference: "browser.megalist.enabled", + }, + AboutLogins: { parent: { esModuleURI: "resource:///actors/AboutLoginsParent.sys.mjs", @@ -2111,7 +2127,7 @@ BrowserGlue.prototype = { () => lazy.BrowserUsageTelemetry.uninit(), () => lazy.SearchSERPTelemetry.uninit(), - () => lazy.SearchSERPDomainToCategoriesMap.uninit(), + () => lazy.SearchSERPCategorization.uninit(), () => lazy.Interactions.uninit(), () => lazy.PageDataService.uninit(), () => lazy.PageThumbs.uninit(), @@ -2341,7 +2357,7 @@ BrowserGlue.prototype = { _badgeIcon(); } - lazy.RemoteSettings(STUDY_ADDON_COLLECTION_KEY).on("sync", async event => { + lazy.RemoteSettings(STUDY_ADDON_COLLECTION_KEY).on("sync", async () => { Services.prefs.setBoolPref(PREF_ION_NEW_STUDIES_AVAILABLE, true); }); @@ -2436,7 +2452,7 @@ BrowserGlue.prototype = { lazy.Sanitizer.onStartup(); this._maybeShowRestoreSessionInfoBar(); this._scheduleStartupIdleTasks(); - this._lateTasksIdleObserver = (idleService, topic, data) => { + this._lateTasksIdleObserver = (idleService, topic) => { if (topic == "idle") { idleService.removeIdleObserver( this._lateTasksIdleObserver, @@ -2662,7 +2678,19 @@ BrowserGlue.prototype = { AppConstants.platform == "win") && Services.prefs.getBoolPref("browser.firefoxbridge.enabled", false), task: async () => { - await lazy.FirefoxBridgeExtensionUtils.ensureRegistered(); + let profileService = Cc[ + "@mozilla.org/toolkit/profile-service;1" + ].getService(Ci.nsIToolkitProfileService); + if ( + profileService.defaultProfile && + profileService.currentProfile == profileService.defaultProfile + ) { + await lazy.FirefoxBridgeExtensionUtils.ensureRegistered(); + } else { + lazy.log.debug( + "FirefoxBridgeExtensionUtils failed to register due to non-default current profile." + ); + } }, }, @@ -3082,9 +3110,16 @@ BrowserGlue.prototype = { }, { - name: "SearchSERPDomainToCategoriesMap.init", + name: "SearchSERPCategorization.init", task: () => { - lazy.SearchSERPDomainToCategoriesMap.init().catch(console.error); + lazy.SearchSERPCategorization.init(); + }, + }, + + { + name: "ContentRelevancyManager.init", + task: () => { + lazy.ContentRelevancyManager.init(); }, }, @@ -3193,12 +3228,6 @@ BrowserGlue.prototype = { lazy.RemoteSecuritySettings.init(); }, - function CorroborateInit() { - if (Services.prefs.getBoolPref("corroborator.enabled", false)) { - lazy.Corroborate.init().catch(console.error); - } - }, - function BrowserUsageTelemetryReportProfileCount() { lazy.BrowserUsageTelemetry.reportProfileCount(); }, @@ -3699,7 +3728,7 @@ BrowserGlue.prototype = { "account-connection-connected", ]); - let clickCallback = (subject, topic, data) => { + let clickCallback = (subject, topic) => { if (topic != "alertclickcallback") { return; } @@ -3740,7 +3769,7 @@ BrowserGlue.prototype = { _migrateUI() { // Use an increasing number to keep track of the current migration state. // Completely unrelated to the current Firefox release number. - const UI_VERSION = 143; + const UI_VERSION = 144; const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL; if (!Services.prefs.prefHasUserValue("browser.migration.version")) { @@ -4374,6 +4403,23 @@ BrowserGlue.prototype = { } } + if (currentUIVersion < 144) { + // TerminatorTelemetry was removed in bug 1879136. Before it was removed, + // the ShutdownDuration.json file would be written to disk at shutdown + // so that the next launch of the browser could read it in and send + // shutdown performance measurements. + // + // Unfortunately, this mechanism and its measurements were fairly + // unreliable, so they were removed. + for (const filename of [ + "ShutdownDuration.json", + "ShutdownDuration.json.tmp", + ]) { + const filePath = PathUtils.join(PathUtils.profileDir, filename); + IOUtils.remove(filePath, { ignoreAbsent: true }).catch(console.error); + } + } + // Update the migration version. Services.prefs.setIntPref("browser.migration.version", UI_VERSION); }, @@ -4462,14 +4508,6 @@ BrowserGlue.prototype = { // Check the default branch as enterprise policies can set prefs there. const defaultPrefs = Services.prefs.getDefaultBranch(""); - if ( - !defaultPrefs.getBoolPref( - "browser.messaging-system.whatsNewPanel.enabled", - true - ) - ) { - return "no-whatsNew"; - } if (!defaultPrefs.getBoolPref("browser.aboutwelcome.enabled", true)) { return "no-welcome"; } @@ -4712,7 +4750,7 @@ BrowserGlue.prototype = { } const title = await lazy.accountsL10n.formatValue(titleL10nId); - const clickCallback = (obsSubject, obsTopic, obsData) => { + const clickCallback = (obsSubject, obsTopic) => { if (obsTopic == "alertclickcallback") { win.gBrowser.selectedTab = firstTab; } @@ -4751,7 +4789,7 @@ BrowserGlue.prototype = { tab = win.gBrowser.addWebTab(url); } tab.attention = true; - let clickCallback = (subject, topic, data) => { + let clickCallback = (subject, topic) => { if (topic != "alertclickcallback") { return; } @@ -4780,7 +4818,7 @@ BrowserGlue.prototype = { : { id: "account-connection-connected-with-noname" }, ]); - let clickCallback = async (subject, topic, data) => { + let clickCallback = async (subject, topic) => { if (topic != "alertclickcallback") { return; } @@ -4815,7 +4853,7 @@ BrowserGlue.prototype = { "account-connection-disconnected", ]); - let clickCallback = (subject, topic, data) => { + let clickCallback = (subject, topic) => { if (topic != "alertclickcallback") { return; } @@ -4870,7 +4908,7 @@ BrowserGlue.prototype = { const TOGGLE_ENABLED_PREF = "media.videocontrols.picture-in-picture.video-toggle.enabled"; - const observe = (subject, topic, data) => { + const observe = (subject, topic) => { const enabled = Services.prefs.getBoolPref(TOGGLE_ENABLED_PREF, false); Services.telemetry.scalarSet("pictureinpicture.toggle_enabled", enabled); @@ -6475,11 +6513,11 @@ export var AboutHomeStartupCache = { /** nsICacheEntryOpenCallback **/ - onCacheEntryCheck(aEntry) { + onCacheEntryCheck() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, - onCacheEntryAvailable(aEntry, aNew, aResult) { + onCacheEntryAvailable(aEntry) { this.log.trace("Cache entry is available."); this._cacheEntry = aEntry; diff --git a/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js b/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js index 5ab03f9867..86e754084b 100644 --- a/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js +++ b/browser/components/aboutlogins/tests/browser/browser_sessionRestore.js @@ -35,7 +35,7 @@ add_task(async function () { createLazyBrowser: true, }); - Assert.equal(lazyTab.linkedPanel, "", "Tab is lazy"); + Assert.equal(lazyTab.linkedPanel, null, "Tab is lazy"); let tabLoaded = new Promise(resolve => { gBrowser.addTabsProgressListener({ async onLocationChange(aBrowser) { diff --git a/browser/components/aboutlogins/tests/browser/head.js b/browser/components/aboutlogins/tests/browser/head.js index 2aec0e632a..82d3cf2062 100644 --- a/browser/components/aboutlogins/tests/browser/head.js +++ b/browser/components/aboutlogins/tests/browser/head.js @@ -131,7 +131,7 @@ add_setup(async function setup_head() { return; } if (msg.errorMessage.includes("Can't find profile directory.")) { - // Ignore error messages for no profile found in old XULStore.jsm + // Ignore error messages for no profile found in old XULStore.sys.mjs return; } if (msg.errorMessage.includes("Error reading typed URL history")) { diff --git a/browser/components/aboutwelcome/.eslintrc.js b/browser/components/aboutwelcome/.eslintrc.js index 1168a8e840..0b8e1cc676 100644 --- a/browser/components/aboutwelcome/.eslintrc.js +++ b/browser/components/aboutwelcome/.eslintrc.js @@ -80,8 +80,6 @@ module.exports = { }, ], rules: { - "fetch-options/no-fetch-credentials": "error", - "react/jsx-boolean-value": ["error", "always"], "react/jsx-key": "error", "react/jsx-no-bind": [ diff --git a/browser/components/aboutwelcome/actors/AboutWelcomeParent.sys.mjs b/browser/components/aboutwelcome/actors/AboutWelcomeParent.sys.mjs index 7b32161e3b..258eff36ef 100644 --- a/browser/components/aboutwelcome/actors/AboutWelcomeParent.sys.mjs +++ b/browser/components/aboutwelcome/actors/AboutWelcomeParent.sys.mjs @@ -70,7 +70,7 @@ class AboutWelcomeObserver { this.win.addEventListener("unload", this.onWindowClose, { once: true }); } - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "quit-application": this.terminateReason = AWTerminate.APP_SHUT_DOWN; diff --git a/browser/components/aboutwelcome/content-src/aboutwelcome.scss b/browser/components/aboutwelcome/content-src/aboutwelcome.scss index 9174fe2439..07bfcd2c96 100644 --- a/browser/components/aboutwelcome/content-src/aboutwelcome.scss +++ b/browser/components/aboutwelcome/content-src/aboutwelcome.scss @@ -1139,6 +1139,38 @@ html { &[no-rdm] { width: 800px; + + &[reverse-split] { + flex-direction: row-reverse; + + .section-main { + .main-content { + border-radius: inherit; + } + + margin: auto; + margin-inline-end: 0; + border-radius: 8px 0 0 8px; + + &:dir(rtl) { + border-radius: 0 8px 8px 0; + margin: auto; + margin-inline-end: 0; + } + } + + .section-secondary { + margin: auto; + margin-inline-start: 0; + border-radius: 0 8px 8px 0; + + &:dir(rtl) { + border-radius: 8px 0 0 8px; + margin: auto; + margin-inline-start: 0; + } + } + } } } @@ -1372,6 +1404,107 @@ html { outline: 2px solid var(--in-content-primary-button-background); } + // newtab wallpaper specific styles + &.wallpaper { + justify-content: center; + gap: 10px; + + &:hover, &:focus-within { + outline: none; + } + + .theme { + flex: unset; + width: unset; + transition: var(--transition); + + &:has(.input:focus) { + outline: 2px solid var(--in-content-primary-button-background); + outline-offset: 2px; + } + + .icon { + width: 116px; + height: 86px; + border-radius: 8px; + box-shadow: 0 1px 2px 0 #3A394433; + + &:hover { + filter: brightness(45%); + } + + // dark theme wallpapers + &.dark-landscape { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif'); + } + + &.dark-beach { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif'); + } + + &.dark-color { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif'); + } + + &.dark-mountain { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif'); + } + + &.dark-panda { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif'); + } + + &.dark-sky { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif'); + } + + // light theme wallpapers + &.light-beach { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif'); + } + + &.light-color { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif'); + } + + &.light-landscape { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif'); + } + + &.light-mountain { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif'); + } + + &.light-panda { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif'); + } + + &.light-sky { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif'); + } + } + } + + .dark { + display: none; + } + + .text { + display: none; + } + + @media (prefers-color-scheme: dark) { + .light { + display: none; + } + + .dark { + display: block; + } + } + + } + .theme { align-items: center; display: flex; @@ -1405,13 +1538,17 @@ html { transform: scaleX(-1); } - &:focus, + &:focus-visible, &:active, &.selected { outline: 2px solid var(--in-content-primary-button-background); outline-offset: 2px; } + &.selected { + outline-color: var(--color-accent-primary-active); + } + &.light { background-image: url('resource://builtin-themes/light/icon.svg'); } diff --git a/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx b/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx index 034055bf3d..3ccbd71f40 100644 --- a/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx +++ b/browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx @@ -463,9 +463,21 @@ export class WelcomeScreen extends React.PureComponent { action.theme === "<event>" ? event.currentTarget.value : this.props.initialTheme || action.theme; - this.props.setActiveTheme(themeToUse); - window.AWSelectTheme(themeToUse); + if (props.content.tiles?.category?.type === "wallpaper") { + const theme = themeToUse.split("-")?.[1]; + let actionWallpaper = { ...props.content.tiles.category.action }; + actionWallpaper.data.actions.forEach(async wpAction => { + if (wpAction.data.pref.name?.includes("dark")) { + wpAction.data.pref.value = `dark-${theme}`; + } else { + wpAction.data.pref.value = `light-${theme}`; + } + await AboutWelcomeUtils.handleUserAction(actionWallpaper); + }); + } else { + window.AWSelectTheme(themeToUse); + } } // If the action has persistActiveTheme: true, we set the initial theme to the currently active theme diff --git a/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx b/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx index 59771e4e48..b6e1ffa6b5 100644 --- a/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx +++ b/browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx @@ -570,32 +570,39 @@ export class ProtonScreen extends React.PureComponent { </div> ) : null} - <div className="main-content-inner"> - <div className={`welcome-text ${content.title_style || ""}`}> - {content.title ? this.renderTitle(content) : null} + <div + className="main-content-inner" + style={{ + justifyContent: content.split_content_justify_content, + }} + > + {content.title || content.subtitle ? ( + <div className={`welcome-text ${content.title_style || ""}`}> + {content.title ? this.renderTitle(content) : null} - {content.subtitle ? ( - <Localized text={content.subtitle}> - <h2 - data-l10n-args={JSON.stringify({ - "addon-name": this.props.addonName, - ...this.props.appAndSystemLocaleInfo?.displayNames, - })} - aria-flowto={ - this.props.messageId?.includes("FEATURE_TOUR") - ? "steps" - : "" - } + {content.subtitle ? ( + <Localized text={content.subtitle}> + <h2 + data-l10n-args={JSON.stringify({ + "addon-name": this.props.addonName, + ...this.props.appAndSystemLocaleInfo?.displayNames, + })} + aria-flowto={ + this.props.messageId?.includes("FEATURE_TOUR") + ? "steps" + : "" + } + /> + </Localized> + ) : null} + {content.cta_paragraph ? ( + <CTAParagraph + content={content.cta_paragraph} + handleAction={this.props.handleAction} /> - </Localized> - ) : null} - {content.cta_paragraph ? ( - <CTAParagraph - content={content.cta_paragraph} - handleAction={this.props.handleAction} - /> - ) : null} - </div> + ) : null} + </div> + ) : null} {content.video_container ? ( <OnboardingVideo content={content.video_container} diff --git a/browser/components/aboutwelcome/content-src/components/Themes.jsx b/browser/components/aboutwelcome/content-src/components/Themes.jsx index 0ee986f982..e430ecf3aa 100644 --- a/browser/components/aboutwelcome/content-src/components/Themes.jsx +++ b/browser/components/aboutwelcome/content-src/components/Themes.jsx @@ -6,28 +6,29 @@ import React from "react"; import { Localized } from "./MSLocalized"; export const Themes = props => { + const category = props.content.tiles?.category?.type; return ( <div className="tiles-theme-container"> <div> - <fieldset className="tiles-theme-section"> + <fieldset className={`tiles-theme-section ${category}`}> <Localized text={props.content.subtitle}> <legend className="sr-only" /> </Localized> {props.content.tiles.data.map( - ({ theme, label, tooltip, description }) => ( + ({ theme, label, tooltip, description, type }) => ( <Localized key={theme + label} text={typeof tooltip === "object" ? tooltip : {}} > {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - <label className="theme" title={theme + label}> + <label className={`theme ${type}`} title={theme + label}> <Localized text={typeof description === "object" ? description : {}} > <input type="radio" value={theme} - name="theme" + name={category === "wallpaper" ? theme : "theme"} checked={theme === props.activeTheme} className="sr-only input" onClick={props.handleAction} diff --git a/browser/components/aboutwelcome/content/aboutwelcome.bundle.js b/browser/components/aboutwelcome/content/aboutwelcome.bundle.js index 0d96257677..11a398e960 100644 --- a/browser/components/aboutwelcome/content/aboutwelcome.bundle.js +++ b/browser/components/aboutwelcome/content/aboutwelcome.bundle.js @@ -560,7 +560,22 @@ class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo if (action.theme) { let themeToUse = action.theme === "<event>" ? event.currentTarget.value : this.props.initialTheme || action.theme; this.props.setActiveTheme(themeToUse); - window.AWSelectTheme(themeToUse); + if (props.content.tiles?.category?.type === "wallpaper") { + const theme = themeToUse.split("-")?.[1]; + let actionWallpaper = { + ...props.content.tiles.category.action + }; + actionWallpaper.data.actions.forEach(async wpAction => { + if (wpAction.data.pref.name?.includes("dark")) { + wpAction.data.pref.value = `dark-${theme}`; + } else { + wpAction.data.pref.value = `light-${theme}`; + } + await _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleUserAction(actionWallpaper); + }); + } else { + window.AWSelectTheme(themeToUse); + } } // If the action has persistActiveTheme: true, we set the initial theme to the currently active theme @@ -1214,8 +1229,11 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom alt: "", role: "presentation" })) : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { - className: "main-content-inner" - }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { + className: "main-content-inner", + style: { + justifyContent: content.split_content_justify_content + } + }, content.title || content.subtitle ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `welcome-text ${content.title_style || ""}` }, content.title ? this.renderTitle(content) : null, content.subtitle ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { text: content.subtitle @@ -1228,7 +1246,7 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom })) : null, content.cta_paragraph ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_CTAParagraph__WEBPACK_IMPORTED_MODULE_8__.CTAParagraph, { content: content.cta_paragraph, handleAction: this.props.handleAction - }) : null), content.video_container ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_OnboardingVideo__WEBPACK_IMPORTED_MODULE_10__.OnboardingVideo, { + }) : null) : null, content.video_container ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_OnboardingVideo__WEBPACK_IMPORTED_MODULE_10__.OnboardingVideo, { content: content.video_container, handleAction: this.props.handleAction }) : null, this.renderContentTiles(), this.renderLanguageSwitcher(), content.above_button_content ? this.renderOrderedContent(content.above_button_content) : null, !hideStepsIndicator && aboveButtonStepsIndicator ? this.renderStepsIndicator() : null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(ProtonScreenActionButtons, { @@ -1448,10 +1466,11 @@ __webpack_require__.r(__webpack_exports__); const Themes = props => { + const category = props.content.tiles?.category?.type; return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "tiles-theme-container" }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("fieldset", { - className: "tiles-theme-section" + className: `tiles-theme-section ${category}` }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { text: props.content.subtitle }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("legend", { @@ -1460,19 +1479,20 @@ const Themes = props => { theme, label, tooltip, - description + description, + type }) => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { key: theme + label, text: typeof tooltip === "object" ? tooltip : {} }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { - className: "theme", + className: `theme ${type}`, title: theme + label }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, { text: typeof description === "object" ? description : {} }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { type: "radio", value: theme, - name: "theme", + name: category === "wallpaper" ? theme : "theme", checked: theme === props.activeTheme, className: "sr-only input", onClick: props.handleAction diff --git a/browser/components/aboutwelcome/content/aboutwelcome.css b/browser/components/aboutwelcome/content/aboutwelcome.css index aa0445e0ef..4e86b29e5d 100644 --- a/browser/components/aboutwelcome/content/aboutwelcome.css +++ b/browser/components/aboutwelcome/content/aboutwelcome.css @@ -1896,6 +1896,32 @@ html { .onboardingContainer .screen[pos=split][no-rdm] { width: 800px; } + .onboardingContainer .screen[pos=split][no-rdm][reverse-split] { + flex-direction: row-reverse; + } + .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-main { + margin: auto; + margin-inline-end: 0; + border-radius: 8px 0 0 8px; + } + .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-main .main-content { + border-radius: inherit; + } + .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-main:dir(rtl) { + border-radius: 0 8px 8px 0; + margin: auto; + margin-inline-end: 0; + } + .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-secondary { + margin: auto; + margin-inline-start: 0; + border-radius: 0 8px 8px 0; + } + .onboardingContainer .screen[pos=split][no-rdm][reverse-split] .section-secondary:dir(rtl) { + border-radius: 8px 0 0 8px; + margin: auto; + margin-inline-start: 0; + } } @media only screen and (height <= 650px) and (800px <= width <= 990px) { .onboardingContainer .screen[pos=split] .section-main .secondary-cta.top { @@ -2091,6 +2117,81 @@ html { border-radius: 8px; outline: 2px solid var(--in-content-primary-button-background); } +.onboardingContainer .tiles-theme-section.wallpaper { + justify-content: center; + gap: 10px; +} +.onboardingContainer .tiles-theme-section.wallpaper:hover, .onboardingContainer .tiles-theme-section.wallpaper:focus-within { + outline: none; +} +.onboardingContainer .tiles-theme-section.wallpaper .theme { + flex: unset; + width: unset; + transition: var(--transition); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme:has(.input:focus) { + outline: 2px solid var(--in-content-primary-button-background); + outline-offset: 2px; +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon { + width: 116px; + height: 86px; + border-radius: 8px; + box-shadow: 0 1px 2px 0 rgba(58, 57, 68, 0.2); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon:hover { + filter: brightness(45%); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .theme .icon.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.onboardingContainer .tiles-theme-section.wallpaper .dark { + display: none; +} +.onboardingContainer .tiles-theme-section.wallpaper .text { + display: none; +} +@media (prefers-color-scheme: dark) { + .onboardingContainer .tiles-theme-section.wallpaper .light { + display: none; + } + .onboardingContainer .tiles-theme-section.wallpaper .dark { + display: block; + } +} .onboardingContainer .tiles-theme-section .theme { align-items: center; display: flex; @@ -2121,10 +2222,13 @@ html { .onboardingContainer .tiles-theme-section .theme .icon:dir(rtl) { transform: scaleX(-1); } -.onboardingContainer .tiles-theme-section .theme .icon:focus, .onboardingContainer .tiles-theme-section .theme .icon:active, .onboardingContainer .tiles-theme-section .theme .icon.selected { +.onboardingContainer .tiles-theme-section .theme .icon:focus-visible, .onboardingContainer .tiles-theme-section .theme .icon:active, .onboardingContainer .tiles-theme-section .theme .icon.selected { outline: 2px solid var(--in-content-primary-button-background); outline-offset: 2px; } +.onboardingContainer .tiles-theme-section .theme .icon.selected { + outline-color: var(--color-accent-primary-active); +} .onboardingContainer .tiles-theme-section .theme .icon.light { background-image: url("resource://builtin-themes/light/icon.svg"); } diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js index 3081688a0c..f3bea5b499 100644 --- a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js +++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js @@ -249,7 +249,7 @@ add_task(async function test_aboutwelcome_with_title_styles() { { "font-weight": "276", "font-size": "36px", - animation: "50s linear 0s infinite normal none running shine", + animation: "50s linear infinite shine", "letter-spacing": "normal", } ); diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_toolbar_button.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_toolbar_button.js index c9180ddf2d..308c27a427 100644 --- a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_toolbar_button.js +++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_toolbar_button.js @@ -32,7 +32,7 @@ add_task(async function test_add_and_remove_toolbar_button() { }); // Open newtab let win = await BrowserTestUtils.openNewBrowserWindow(); - win.BrowserOpenTab(); + win.BrowserCommands.openTab(); ok(win, "browser exists"); // Try to add the button. It shouldn't add because the pref is false await AWToolbarButton.maybeAddSetupButton(); diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx index 9b452d5c6b..ed0260bf30 100644 --- a/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx +++ b/browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx @@ -671,4 +671,27 @@ describe("MultiStageAboutWelcomeProton module", () => { assert.isTrue(wrapper.find("migration-wizard").exists()); }); }); + + describe("Custom main content inner custom justify content", () => { + const SCREEN_PROPS = { + content: { + title: "test title", + position: "split", + split_content_justify_content: "flex-start", + }, + }; + + it("should render split screen with custom justify-content", async () => { + const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />); + assert.ok(wrapper.exists()); + assert.equal(wrapper.find("main").prop("pos"), "split"); + assert.exists(wrapper.find(".main-content-inner")); + assert.ok( + wrapper + .find(".main-content-inner") + .prop("style") + .justifyContent.includes("flex-start") + ); + }); + }); }); diff --git a/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx index b4593a45f3..2fb897125d 100644 --- a/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx +++ b/browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx @@ -383,6 +383,72 @@ describe("MultiStageAboutWelcome module", () => { ); }); }); + + describe("Wallpaper screen", () => { + let WALLPAPER_SCREEN_PROPS; + beforeEach(() => { + WALLPAPER_SCREEN_PROPS = { + content: { + title: "test title", + subtitle: "test subtitle", + tiles: { + type: "theme", + category: { + type: "wallpaper", + action: { + type: "MULTI_ACTION", + data: { + actions: [ + { + type: "SET_PREF", + data: { + pref: { + name: "test-dark", + }, + }, + }, + { + type: "SET_PREF", + data: { + pref: { + name: "test-light", + }, + }, + }, + ], + }, + }, + }, + action: { + theme: "<event>", + }, + data: [ + { + theme: "mountain", + type: "light", + }, + ], + }, + primary_button: { + action: {}, + label: "test button", + }, + }, + navigate: sandbox.stub(), + setActiveTheme: sandbox.stub(), + }; + sandbox.stub(AboutWelcomeUtils, "handleUserAction").resolves(); + }); + it("should handle wallpaper click", () => { + const wrapper = mount(<WelcomeScreen {...WALLPAPER_SCREEN_PROPS} />); + const wallpaperOptions = wrapper.find( + ".tiles-theme-section .theme input[name='mountain']" + ); + wallpaperOptions.simulate("click"); + assert.calledTwice(AboutWelcomeUtils.handleUserAction); + }); + }); + describe("#handleAction", () => { let SCREEN_PROPS; let TEST_ACTION; diff --git a/browser/components/aboutwelcome/tests/unit/unit-entry.js b/browser/components/aboutwelcome/tests/unit/unit-entry.js index fb70eeb843..3da6964c53 100644 --- a/browser/components/aboutwelcome/tests/unit/unit-entry.js +++ b/browser/components/aboutwelcome/tests/unit/unit-entry.js @@ -97,8 +97,8 @@ const TEST_GLOBAL = { JSWindowActorParent, JSWindowActorChild, AboutReaderParent: { - addMessageListener: (messageName, listener) => {}, - removeMessageListener: (messageName, listener) => {}, + addMessageListener: (_messageName, _listener) => {}, + removeMessageListener: (_messageName, _listener) => {}, }, AboutWelcomeTelemetry: class { submitGleanPingForPing() {} @@ -281,8 +281,8 @@ const TEST_GLOBAL = { }, dump() {}, EveryWindow: { - registerCallback: (id, init, uninit) => {}, - unregisterCallback: id => {}, + registerCallback: (_id, _init, _uninit) => {}, + unregisterCallback: _id => {}, }, setTimeout: window.setTimeout.bind(window), clearTimeout: window.clearTimeout.bind(window), @@ -402,7 +402,7 @@ const TEST_GLOBAL = { }, urlFormatter: { formatURL: str => str, formatURLPref: str => str }, mm: { - addMessageListener: (msg, cb) => this.receiveMessage(), + addMessageListener: (_msg, _cb) => this.receiveMessage(), removeMessageListener() {}, }, obs: { @@ -412,7 +412,7 @@ const TEST_GLOBAL = { }, telemetry: { setEventRecordingEnabled: () => {}, - recordEvent: eventDetails => {}, + recordEvent: _eventDetails => {}, scalarSet: () => {}, keyedScalarAdd: () => {}, }, @@ -570,7 +570,7 @@ const TEST_GLOBAL = { finish: () => {}, }, Sampling: { - ratioSample(seed, ratios) { + ratioSample(_seed, _ratios) { return Promise.resolve(0); }, }, diff --git a/browser/components/asrouter/.eslintrc.js b/browser/components/asrouter/.eslintrc.js index ef5bc81b68..b2a647e42d 100644 --- a/browser/components/asrouter/.eslintrc.js +++ b/browser/components/asrouter/.eslintrc.js @@ -63,8 +63,6 @@ module.exports = { }, ], rules: { - "fetch-options/no-fetch-credentials": "error", - "react/jsx-boolean-value": ["error", "always"], "react/jsx-key": "error", "react/jsx-no-bind": [ diff --git a/browser/components/asrouter/actors/ASRouterChild.sys.mjs b/browser/components/asrouter/actors/ASRouterChild.sys.mjs index 2096d92bb3..95f625e2b5 100644 --- a/browser/components/asrouter/actors/ASRouterChild.sys.mjs +++ b/browser/components/asrouter/actors/ASRouterChild.sys.mjs @@ -11,9 +11,7 @@ // eslint-disable-next-line mozilla/use-static-import const { MESSAGE_TYPE_LIST, MESSAGE_TYPE_HASH: msg } = - ChromeUtils.importESModule( - "resource:///modules/asrouter/ActorConstants.sys.mjs" - ); + ChromeUtils.importESModule("resource:///modules/asrouter/ActorConstants.mjs"); const VALID_TYPES = new Set(MESSAGE_TYPE_LIST); @@ -103,8 +101,6 @@ export class ASRouterChild extends JSWindowActorChild { case msg.DISABLE_PROVIDER: case msg.ENABLE_PROVIDER: case msg.EXPIRE_QUERY_CACHE: - case msg.FORCE_WHATSNEW_PANEL: - case msg.CLOSE_WHATSNEW_PANEL: case msg.FORCE_PRIVATE_BROWSING_WINDOW: case msg.IMPRESSION: case msg.RESET_PROVIDER_PREF: diff --git a/browser/components/asrouter/bin/import-rollouts.js b/browser/components/asrouter/bin/import-rollouts.js index d29a31a068..bb5c17d9ae 100644 --- a/browser/components/asrouter/bin/import-rollouts.js +++ b/browser/components/asrouter/bin/import-rollouts.js @@ -126,10 +126,6 @@ async function getMessageValidators(skipValidation) { "./content-src/templates/OnboardingMessage/UpdateAction.schema.json", { common: true } ), - whatsnew_panel_message: await getValidator( - "./content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json", - { common: true } - ), feature_callout: await getValidator( // For now, Feature Callout and Spotlight share a common schema "./content-src/templates/OnboardingMessage/Spotlight.schema.json", diff --git a/browser/components/asrouter/content-src/asrouter-utils.mjs b/browser/components/asrouter/content-src/asrouter-utils.mjs index 989d864e71..3789158547 100644 --- a/browser/components/asrouter/content-src/asrouter-utils.mjs +++ b/browser/components/asrouter/content-src/asrouter-utils.mjs @@ -2,10 +2,8 @@ * 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/. */ -// eslint-disable-next-line mozilla/reject-import-system-module-from-non-system -import { MESSAGE_TYPE_HASH as msg } from "../modules/ActorConstants.sys.mjs"; -// eslint-disable-next-line mozilla/reject-import-system-module-from-non-system -import { actionCreators as ac } from "../../newtab/common/Actions.sys.mjs"; +import { MESSAGE_TYPE_HASH as msg } from "../modules/ActorConstants.mjs"; +import { actionCreators as ac } from "../../newtab/common/Actions.mjs"; export const ASRouterUtils = { addListener(listener) { diff --git a/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx b/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx index befce707ef..32d1614307 100644 --- a/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx +++ b/browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx @@ -15,6 +15,18 @@ const Row = props => ( </tr> ); +// Convert a UTF-8 string to a string in which only one byte of each +// 16-bit unit is occupied. This is necessary to comply with `btoa` API constraints. +export function toBinary(string) { + const codeUnits = new Uint16Array(string.length); + for (let i = 0; i < codeUnits.length; i++) { + codeUnits[i] = string.charCodeAt(i); + } + return btoa( + String.fromCharCode(...Array.from(new Uint8Array(codeUnits.buffer))) + ); +} + function relativeTime(timestamp) { if (!timestamp) { return ""; @@ -531,7 +543,9 @@ export class ASRouterAdminInner extends React.PureComponent { {aboutMessagePreviewSupported ? ( <CopyButton transformer={text => - `about:messagepreview?json=${encodeURIComponent(btoa(text))}` + `about:messagepreview?json=${encodeURIComponent( + toBinary(text) + )}` } label="Share" copiedLabel="Copied!" diff --git a/browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json b/browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json index 9de01052f7..5fe86f9617 100644 --- a/browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json +++ b/browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json @@ -10,7 +10,9 @@ "const": "multi" } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/MultiMessage" @@ -68,7 +70,9 @@ "type": "object" } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": true }, "requireInteraction": { @@ -116,24 +120,37 @@ "type": "object" } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": true } }, - "required": ["action", "title"], + "required": [ + "action", + "title" + ], "additionalProperties": true } } }, "additionalProperties": true, - "required": ["title", "body"] + "required": [ + "title", + "body" + ] }, "template": { "type": "string", "const": "toast_notification" } }, - "required": ["content", "targeting", "template", "trigger"], + "required": [ + "content", + "targeting", + "template", + "trigger" + ], "additionalProperties": true }, "Message": { @@ -154,7 +171,9 @@ "template": { "type": "string", "description": "Which messaging template this message is using.", - "enum": ["toast_notification"] + "enum": [ + "toast_notification" + ] }, "frequency": { "type": "object", @@ -184,7 +203,10 @@ "maximum": 100 } }, - "required": ["period", "cap"] + "required": [ + "period", + "cap" + ] } } } @@ -224,7 +246,9 @@ } } }, - "required": ["id"] + "required": [ + "id" + ] }, "provider": { "description": "An identifier for the provider of this message, such as \"cfr\" or \"preview\".", @@ -233,8 +257,14 @@ }, "additionalProperties": true, "dependentRequired": { - "content": ["id", "template"], - "template": ["id", "content"] + "content": [ + "id", + "template" + ], + "template": [ + "id", + "content" + ] } }, "localizedText": { @@ -245,7 +275,9 @@ "type": "string" } }, - "required": ["string_id"] + "required": [ + "string_id" + ] }, "localizableText": { "description": "Either a raw string or an object containing the string_id of the localized text", @@ -272,10 +304,14 @@ "properties": { "template": { "type": "string", - "enum": ["toast_notification"] + "enum": [ + "toast_notification" + ] } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json#/$defs/ToastNotification" @@ -299,7 +335,10 @@ } } }, - "required": ["template", "messages"] + "required": [ + "template", + "messages" + ] } } } diff --git a/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json b/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json index fbabb109f8..dd4ce4776d 100644 --- a/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json +++ b/browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json @@ -10,7 +10,9 @@ "const": "multi" } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/MultiMessage" @@ -41,7 +43,9 @@ "layout": { "type": "string", "description": "Describes how content should be displayed.", - "enum": ["chiclet_open_url"] + "enum": [ + "chiclet_open_url" + ] }, "bucket_id": { "type": "string", @@ -66,11 +70,17 @@ "where": { "description": "Should it open in a new tab or the current tab", "type": "string", - "enum": ["current", "tabshifted"] + "enum": [ + "current", + "tabshifted" + ] } }, "additionalProperties": true, - "required": ["url", "where"] + "required": [ + "url", + "where" + ] } }, "additionalProperties": true, @@ -87,7 +97,10 @@ "const": "cfr_urlbar_chiclet" } }, - "required": ["targeting", "trigger"] + "required": [ + "targeting", + "trigger" + ] }, "ExtensionDoorhanger": { "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -162,10 +175,14 @@ "description": "Text for button tooltip used to provide information about the doorhanger." } }, - "required": ["tooltiptext"] + "required": [ + "tooltiptext" + ] } }, - "required": ["attributes"] + "required": [ + "attributes" + ] }, { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizedText" @@ -185,7 +202,10 @@ "learn_more": { "type": "string", "description": "Last part of the path in the SUMO URL to the support page with the information about the doorhanger.", - "examples": ["extensionpromotions", "extensionrecommendations"] + "examples": [ + "extensionpromotions", + "extensionrecommendations" + ] }, "heading_text": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText", @@ -236,7 +256,12 @@ "description": "Link that offers more information related to the addon." } }, - "required": ["title", "author", "icon", "amo_url"] + "required": [ + "title", + "author", + "icon", + "amo_url" + ] }, "text": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText", @@ -255,7 +280,9 @@ } } }, - "required": ["steps"] + "required": [ + "steps" + ] }, "buttons": { "description": "The label and functionality for the buttons in the pop-over.", @@ -281,11 +308,16 @@ "description": "A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label." } }, - "required": ["accesskey"], + "required": [ + "accesskey" + ], "description": "Button attributes." } }, - "required": ["value", "attributes"] + "required": [ + "value", + "attributes" + ] }, { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizedText" @@ -341,11 +373,16 @@ "description": "A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label." } }, - "required": ["accesskey"], + "required": [ + "accesskey" + ], "description": "Button attributes." } }, - "required": ["value", "attributes"] + "required": [ + "value", + "attributes" + ] }, { "properties": { @@ -360,7 +397,9 @@ ] } }, - "required": ["string_id"] + "required": [ + "string_id" + ] } ], "description": "Id of localized string or message override." @@ -417,16 +456,25 @@ } }, "then": { - "required": ["category", "notification_text"] + "required": [ + "category", + "notification_text" + ] } }, "template": { "type": "string", - "enum": ["cfr_doorhanger", "milestone_message"] + "enum": [ + "cfr_doorhanger", + "milestone_message" + ] } }, "additionalProperties": true, - "required": ["targeting", "trigger"], + "required": [ + "targeting", + "trigger" + ], "$defs": { "plainText": { "description": "Plain text (no HTML allowed)", @@ -457,7 +505,10 @@ "type": { "type": "string", "description": "Should the message be global (persisted across tabs) or local (disappear when switching to a different tab).", - "enum": ["global", "tab"] + "enum": [ + "global", + "tab" + ] }, "text": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText", @@ -497,7 +548,9 @@ "type": "object" } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": true }, "supportPage": { @@ -505,13 +558,19 @@ "description": "A page title on SUMO to link to" } }, - "required": ["label", "action"], + "required": [ + "label", + "action" + ], "additionalProperties": true } } }, "additionalProperties": true, - "required": ["text", "buttons"] + "required": [ + "text", + "buttons" + ] }, "template": { "type": "string", @@ -519,7 +578,10 @@ } }, "additionalProperties": true, - "required": ["targeting", "trigger"], + "required": [ + "targeting", + "trigger" + ], "$defs": { "plainText": { "description": "Plain text (no HTML allowed)", @@ -587,12 +649,22 @@ "promoType": { "type": "string", "description": "Promo type used to determine if promo should show to a given user", - "enum": ["FOCUS", "VPN", "PIN", "COOKIE_BANNERS", "OTHER"] + "enum": [ + "FOCUS", + "VPN", + "PIN", + "COOKIE_BANNERS", + "OTHER" + ] }, "promoSectionStyle": { "type": "string", "description": "Sets the position of the promo section. Possible values are: top, below-search, bottom. Default bottom.", - "enum": ["top", "below-search", "bottom"] + "enum": [ + "top", + "below-search", + "bottom" + ] }, "promoTitle": { "type": "string", @@ -624,16 +696,23 @@ "type": "object" } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": true } }, - "required": ["action"] + "required": [ + "action" + ] }, "promoLinkType": { "type": "string", "description": "Type of promo link type. Possible values: link, button. Default is link.", - "enum": ["link", "button"] + "enum": [ + "link", + "button" + ] }, "promoImageLarge": { "type": "string", @@ -655,10 +734,14 @@ "const": true } }, - "required": ["promoEnabled"] + "required": [ + "promoEnabled" + ] }, "then": { - "required": ["promoButton"] + "required": [ + "promoButton" + ] } }, { @@ -668,20 +751,28 @@ "const": true } }, - "required": ["infoEnabled"] + "required": [ + "infoEnabled" + ] }, "then": { - "required": ["infoLinkText"], + "required": [ + "infoLinkText" + ], "if": { "properties": { "infoTitleEnabled": { "const": true } }, - "required": ["infoTitleEnabled"] + "required": [ + "infoTitleEnabled" + ] }, "then": { - "required": ["infoTitle"] + "required": [ + "infoTitle" + ] } } } @@ -693,7 +784,9 @@ } }, "additionalProperties": true, - "required": ["targeting"] + "required": [ + "targeting" + ] }, "Spotlight": { "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -763,11 +856,16 @@ "template": { "type": "string", "description": "Specify whether the surface is shown as a Spotlight modal or an in-surface Feature Callout dialog", - "enum": ["spotlight", "feature_callout"] + "enum": [ + "spotlight", + "feature_callout" + ] } }, "additionalProperties": true, - "required": ["targeting"] + "required": [ + "targeting" + ] }, "ToastNotification": { "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -818,7 +916,9 @@ "type": "object" } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": true }, "requireInteraction": { @@ -866,24 +966,37 @@ "type": "object" } }, - "required": ["type"], + "required": [ + "type" + ], "additionalProperties": true } }, - "required": ["action", "title"], + "required": [ + "action", + "title" + ], "additionalProperties": true } } }, "additionalProperties": true, - "required": ["title", "body"] + "required": [ + "title", + "body" + ] }, "template": { "type": "string", "const": "toast_notification" } }, - "required": ["content", "targeting", "template", "trigger"], + "required": [ + "content", + "targeting", + "template", + "trigger" + ], "additionalProperties": true }, "ToolbarBadgeMessage": { @@ -912,7 +1025,9 @@ } }, "additionalProperties": true, - "required": ["id"], + "required": [ + "id" + ], "description": "Optional action to take in addition to showing the notification" }, "delay": { @@ -925,7 +1040,9 @@ } }, "additionalProperties": true, - "required": ["target"] + "required": [ + "target" + ] }, "template": { "type": "string", @@ -933,7 +1050,9 @@ } }, "additionalProperties": true, - "required": ["targeting"] + "required": [ + "targeting" + ] }, "UpdateAction": { "$schema": "https://json-schema.org/draft/2019-09/schema", @@ -973,101 +1092,25 @@ }, "additionalProperties": true, "description": "Optional action to take in addition to showing the notification", - "required": ["id", "data"] - } - }, - "additionalProperties": true, - "required": ["action"] - }, - "template": { - "type": "string", - "const": "update_action" - } - }, - "required": ["targeting"] - }, - "WhatsNewMessage": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "file:///WhatsNewMessage.schema.json", - "title": "WhatsNewMessage", - "description": "A template for the messages that appear in the What's New panel.", - "allOf": [ - { - "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/Message" - } - ], - "type": "object", - "properties": { - "content": { - "type": "object", - "properties": { - "layout": { - "description": "Different message layouts", - "enum": ["tracking-protections"] - }, - "bucket_id": { - "type": "string", - "description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting." - }, - "published_date": { - "type": "integer", - "description": "The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published." - }, - "title": { - "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText", - "description": "Id of localized string or message override of What's New message title" - }, - "subtitle": { - "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText", - "description": "Id of localized string or message override of What's New message subtitle" - }, - "body": { - "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText", - "description": "Id of localized string or message override of What's New message body" - }, - "link_text": { - "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText", - "description": "(optional) Id of localized string or message override of What's New message link text" - }, - "cta_url": { - "description": "Target URL for the What's New message.", - "type": "string", - "format": "moz-url-format" - }, - "cta_type": { - "description": "Type of url open action", - "enum": ["OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PROTECTION_REPORT"] - }, - "cta_where": { - "description": "How to open the cta: new window, tab, focused, unfocused.", - "enum": ["current", "tabshifted", "tab", "save", "window"] - }, - "icon_url": { - "description": "(optional) URL for the What's New message icon.", - "type": "string", - "format": "uri" - }, - "icon_alt": { - "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/localizableText", - "description": "Alt text for image." + "required": [ + "id", + "data" + ] } }, "additionalProperties": true, "required": [ - "published_date", - "title", - "body", - "cta_url", - "bucket_id" + "action" ] }, "template": { "type": "string", - "const": "whatsnew_panel_message" + "const": "update_action" } }, - "required": ["order"], - "additionalProperties": true + "required": [ + "targeting" + ] }, "Message": { "type": "object", @@ -1097,8 +1140,7 @@ "feature_callout", "toast_notification", "toolbar_badge", - "update_action", - "whatsnew_panel_message" + "update_action" ] }, "frequency": { @@ -1129,7 +1171,10 @@ "maximum": 100 } }, - "required": ["period", "cap"] + "required": [ + "period", + "cap" + ] } } } @@ -1169,7 +1214,9 @@ } } }, - "required": ["id"] + "required": [ + "id" + ] }, "provider": { "description": "An identifier for the provider of this message, such as \"cfr\" or \"preview\".", @@ -1178,8 +1225,14 @@ }, "additionalProperties": true, "dependentRequired": { - "content": ["id", "template"], - "template": ["id", "content"] + "content": [ + "id", + "template" + ], + "template": [ + "id", + "content" + ] } }, "localizedText": { @@ -1190,7 +1243,9 @@ "type": "string" } }, - "required": ["string_id"] + "required": [ + "string_id" + ] }, "localizableText": { "description": "Either a raw string or an object containing the string_id of the localized text", @@ -1217,10 +1272,14 @@ "properties": { "template": { "type": "string", - "enum": ["cfr_urlbar_chiclet"] + "enum": [ + "cfr_urlbar_chiclet" + ] } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/CFRUrlbarChiclet" @@ -1232,10 +1291,15 @@ "properties": { "template": { "type": "string", - "enum": ["cfr_doorhanger", "milestone_message"] + "enum": [ + "cfr_doorhanger", + "milestone_message" + ] } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/ExtensionDoorhanger" @@ -1247,10 +1311,14 @@ "properties": { "template": { "type": "string", - "enum": ["infobar"] + "enum": [ + "infobar" + ] } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/InfoBar" @@ -1262,10 +1330,14 @@ "properties": { "template": { "type": "string", - "enum": ["pb_newtab"] + "enum": [ + "pb_newtab" + ] } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/NewtabPromoMessage" @@ -1277,10 +1349,15 @@ "properties": { "template": { "type": "string", - "enum": ["spotlight", "feature_callout"] + "enum": [ + "spotlight", + "feature_callout" + ] } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/Spotlight" @@ -1292,10 +1369,14 @@ "properties": { "template": { "type": "string", - "enum": ["toast_notification"] + "enum": [ + "toast_notification" + ] } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/ToastNotification" @@ -1307,10 +1388,14 @@ "properties": { "template": { "type": "string", - "enum": ["toolbar_badge"] + "enum": [ + "toolbar_badge" + ] } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/ToolbarBadgeMessage" @@ -1322,29 +1407,18 @@ "properties": { "template": { "type": "string", - "enum": ["update_action"] + "enum": [ + "update_action" + ] } }, - "required": ["template"] + "required": [ + "template" + ] }, "then": { "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/UpdateAction" } - }, - { - "if": { - "type": "object", - "properties": { - "template": { - "type": "string", - "enum": ["whatsnew_panel_message"] - } - }, - "required": ["template"] - }, - "then": { - "$ref": "chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json#/$defs/WhatsNewMessage" - } } ] }, @@ -1364,7 +1438,10 @@ } } }, - "required": ["template", "messages"] + "required": [ + "template", + "messages" + ] } } } diff --git a/browser/components/asrouter/content-src/schemas/make-schemas.py b/browser/components/asrouter/content-src/schemas/make-schemas.py index f66490f23a..1f677cab28 100755 --- a/browser/components/asrouter/content-src/schemas/make-schemas.py +++ b/browser/components/asrouter/content-src/schemas/make-schemas.py @@ -83,9 +83,6 @@ SCHEMAS = [ "UpdateAction": ( SCHEMA_DIR / "OnboardingMessage" / "UpdateAction.schema.json" ), - "WhatsNewMessage": ( - SCHEMA_DIR / "OnboardingMessage" / "WhatsNewMessage.schema.json" - ), }, bundle_common=True, test_corpus={ diff --git a/browser/components/asrouter/content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json b/browser/components/asrouter/content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json deleted file mode 100644 index 26e795d068..0000000000 --- a/browser/components/asrouter/content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "file:///WhatsNewMessage.schema.json", - "title": "WhatsNewMessage", - "description": "A template for the messages that appear in the What's New panel.", - "allOf": [{ "$ref": "file:///FxMSCommon.schema.json#/$defs/Message" }], - "type": "object", - "properties": { - "content": { - "type": "object", - "properties": { - "layout": { - "description": "Different message layouts", - "enum": ["tracking-protections"] - }, - "bucket_id": { - "type": "string", - "description": "A bucket identifier for the addon. This is used in order to anonymize telemetry for history-sensitive targeting." - }, - "published_date": { - "type": "integer", - "description": "The date/time (number of milliseconds elapsed since January 1, 1970 00:00:00 UTC) the message was published." - }, - "title": { - "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", - "description": "Id of localized string or message override of What's New message title" - }, - "subtitle": { - "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", - "description": "Id of localized string or message override of What's New message subtitle" - }, - "body": { - "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", - "description": "Id of localized string or message override of What's New message body" - }, - "link_text": { - "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", - "description": "(optional) Id of localized string or message override of What's New message link text" - }, - "cta_url": { - "description": "Target URL for the What's New message.", - "type": "string", - "format": "moz-url-format" - }, - "cta_type": { - "description": "Type of url open action", - "enum": ["OPEN_URL", "OPEN_ABOUT_PAGE", "OPEN_PROTECTION_REPORT"] - }, - "cta_where": { - "description": "How to open the cta: new window, tab, focused, unfocused.", - "enum": ["current", "tabshifted", "tab", "save", "window"] - }, - "icon_url": { - "description": "(optional) URL for the What's New message icon.", - "type": "string", - "format": "uri" - }, - "icon_alt": { - "$ref": "file:///FxMSCommon.schema.json#/$defs/localizableText", - "description": "Alt text for image." - } - }, - "additionalProperties": true, - "required": ["published_date", "title", "body", "cta_url", "bucket_id"] - }, - "template": { - "type": "string", - "const": "whatsnew_panel_message" - } - }, - "required": ["order"], - "additionalProperties": true -} diff --git a/browser/components/asrouter/content/asrouter-admin.bundle.js b/browser/components/asrouter/content/asrouter-admin.bundle.js index b38d551a17..e5a1e6dd6a 100644 --- a/browser/components/asrouter/content/asrouter-admin.bundle.js +++ b/browser/components/asrouter/content/asrouter-admin.bundle.js @@ -16,15 +16,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ ASRouterUtils: () => (/* binding */ ASRouterUtils) /* harmony export */ }); -/* harmony import */ var _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); -/* harmony import */ var _newtab_common_Actions_sys_mjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3); +/* harmony import */ var _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); +/* harmony import */ var _newtab_common_Actions_mjs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3); /* 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/. */ -// eslint-disable-next-line mozilla/reject-import-system-module-from-non-system -// eslint-disable-next-line mozilla/reject-import-system-module-from-non-system const ASRouterUtils = { @@ -46,54 +44,54 @@ const ASRouterUtils = { }, blockById(id, options) { return ASRouterUtils.sendMessage({ - type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.BLOCK_MESSAGE_BY_ID, + type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.BLOCK_MESSAGE_BY_ID, data: { id, ...options }, }); }, modifyMessageJson(content) { return ASRouterUtils.sendMessage({ - type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.MODIFY_MESSAGE_JSON, + type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.MODIFY_MESSAGE_JSON, data: { content }, }); }, executeAction(button_action) { return ASRouterUtils.sendMessage({ - type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.USER_ACTION, + type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.USER_ACTION, data: button_action, }); }, unblockById(id) { return ASRouterUtils.sendMessage({ - type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.UNBLOCK_MESSAGE_BY_ID, + type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.UNBLOCK_MESSAGE_BY_ID, data: { id }, }); }, blockBundle(bundle) { return ASRouterUtils.sendMessage({ - type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.BLOCK_BUNDLE, + type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.BLOCK_BUNDLE, data: { bundle }, }); }, unblockBundle(bundle) { return ASRouterUtils.sendMessage({ - type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.UNBLOCK_BUNDLE, + type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.UNBLOCK_BUNDLE, data: { bundle }, }); }, overrideMessage(id) { return ASRouterUtils.sendMessage({ - type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.OVERRIDE_MESSAGE, + type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.OVERRIDE_MESSAGE, data: { id }, }); }, editState(key, value) { return ASRouterUtils.sendMessage({ - type: _modules_ActorConstants_sys_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.EDIT_STATE, + type: _modules_ActorConstants_mjs__WEBPACK_IMPORTED_MODULE_0__.MESSAGE_TYPE_HASH.EDIT_STATE, data: { [key]: value }, }); }, sendTelemetry(ping) { - return ASRouterUtils.sendMessage(_newtab_common_Actions_sys_mjs__WEBPACK_IMPORTED_MODULE_1__.actionCreators.ASRouterUserEvent(ping)); + return ASRouterUtils.sendMessage(_newtab_common_Actions_mjs__WEBPACK_IMPORTED_MODULE_1__.actionCreators.ASRouterUserEvent(ping)); }, getPreviewEndpoint() { return null; @@ -124,7 +122,6 @@ const MESSAGE_TYPE_LIST = [ "PBNEWTAB_MESSAGE_REQUEST", "DOORHANGER_TELEMETRY", "TOOLBAR_BADGE_TELEMETRY", - "TOOLBAR_PANEL_TELEMETRY", "MOMENTS_PAGE_TELEMETRY", "INFOBAR_TELEMETRY", "SPOTLIGHT_TELEMETRY", @@ -142,9 +139,7 @@ const MESSAGE_TYPE_LIST = [ "EVALUATE_JEXL_EXPRESSION", "EXPIRE_QUERY_CACHE", "FORCE_ATTRIBUTION", - "FORCE_WHATSNEW_PANEL", "FORCE_PRIVATE_BROWSING_WINDOW", - "CLOSE_WHATSNEW_PANEL", "OVERRIDE_MESSAGE", "MODIFY_MESSAGE_JSON", "RESET_PROVIDER_PREF", @@ -181,6 +176,8 @@ __webpack_require__.r(__webpack_exports__); * 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/. */ +// This file is accessed from both content and system scopes. + const MAIN_MESSAGE_TYPE = "ActivityStream:Main"; const CONTENT_MESSAGE_TYPE = "ActivityStream:Content"; const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser"; @@ -337,6 +334,7 @@ for (const type of [ "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", + "WALLPAPERS_SET", "WEBEXT_CLICK", "WEBEXT_DISMISS", ]) { @@ -550,8 +548,11 @@ function DiscoveryStreamLoadedContent( return importContext === UI_CODE ? AlsoToMain(action) : action; } -function SetPref(name, value, importContext = globalImportContext) { - const action = { type: actionTypes.SET_PREF, data: { name, value } }; +function SetPref(prefName, value, importContext = globalImportContext) { + const action = { + type: actionTypes.SET_PREF, + data: { name: prefName, value }, + }; return importContext === UI_CODE ? AlsoToMain(action) : action; } @@ -961,7 +962,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ ToggleMessageJSON: () => (/* binding */ ToggleMessageJSON), /* harmony export */ TogglePrefCheckbox: () => (/* binding */ TogglePrefCheckbox), /* harmony export */ ToggleStoryButton: () => (/* binding */ ToggleStoryButton), -/* harmony export */ renderASRouterAdmin: () => (/* binding */ renderASRouterAdmin) +/* harmony export */ renderASRouterAdmin: () => (/* binding */ renderASRouterAdmin), +/* harmony export */ toBinary: () => (/* binding */ toBinary) /* harmony export */ }); /* harmony import */ var _asrouter_utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -985,6 +987,16 @@ function _extends() { _extends = Object.assign ? Object.assign.bind() : function const Row = props => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement("tr", _extends({ className: "message-item" }, props), props.children); + +// Convert a UTF-8 string to a string in which only one byte of each +// 16-bit unit is occupied. This is necessary to comply with `btoa` API constraints. +function toBinary(string) { + const codeUnits = new Uint16Array(string.length); + for (let i = 0; i < codeUnits.length; i++) { + codeUnits[i] = string.charCodeAt(i); + } + return btoa(String.fromCharCode(...Array.from(new Uint8Array(codeUnits.buffer)))); +} function relativeTime(timestamp) { if (!timestamp) { return ""; @@ -1427,7 +1439,7 @@ class ASRouterAdminInner extends (react__WEBPACK_IMPORTED_MODULE_1___default().P className: "button modify", onClick: () => this.modifyJson(msg) }, "Modify"), aboutMessagePreviewSupported ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(_CopyButton__WEBPACK_IMPORTED_MODULE_4__.CopyButton, { - transformer: text => `about:messagepreview?json=${encodeURIComponent(btoa(text))}`, + transformer: text => `about:messagepreview?json=${encodeURIComponent(toBinary(text))}`, label: "Share", copiedLabel: "Copied!", inputSelector: `#${msg.id}-textarea`, diff --git a/browser/components/asrouter/docs/targeting-attributes.md b/browser/components/asrouter/docs/targeting-attributes.md index 89c5a6b6c6..b0049a4f1b 100644 --- a/browser/components/asrouter/docs/targeting-attributes.md +++ b/browser/components/asrouter/docs/targeting-attributes.md @@ -44,7 +44,6 @@ Please note that some targeting attributes require stricter controls on the tele * [isFxASignedIn](#isFxASignedIn) * [isMajorUpgrade](#ismajorupgrade) * [isRTAMO](#isrtamo) -* [isWhatsNewPanelEnabled](#iswhatsnewpanelenabled) * [launchOnLoginEnabled](#launchonloginenabled) * [locale](#locale) * [localeLanguageCode](#localelanguagecode) @@ -630,16 +629,6 @@ Boolean pref that gets set the first time the user opens the FxA toolbar panel declare const hasAccessedFxAPanel: boolean; ``` -### `isWhatsNewPanelEnabled` - -Boolean pref that controls if the What's New panel feature is enabled - -#### Definition - -```ts -declare const isWhatsNewPanelEnabled: boolean; -``` - ### `totalBlockedCount` Total number of events from the content blocking database diff --git a/browser/components/asrouter/modules/ASRouter.sys.mjs b/browser/components/asrouter/modules/ASRouter.sys.mjs index e46c57f685..b36a9023e1 100644 --- a/browser/components/asrouter/modules/ASRouter.sys.mjs +++ b/browser/components/asrouter/modules/ASRouter.sys.mjs @@ -55,7 +55,6 @@ ChromeUtils.defineESModuleGetters(lazy, { Spotlight: "resource:///modules/asrouter/Spotlight.sys.mjs", ToastNotification: "resource:///modules/asrouter/ToastNotification.sys.mjs", ToolbarBadgeHub: "resource:///modules/asrouter/ToolbarBadgeHub.sys.mjs", - ToolbarPanelHub: "resource:///modules/asrouter/ToolbarPanelHub.sys.mjs", }); XPCOMUtils.defineLazyServiceGetters(lazy, { @@ -67,7 +66,7 @@ ChromeUtils.defineLazyGetter(lazy, "log", () => { ); return new Logger("ASRouter"); }); -import { actionCreators as ac } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionCreators as ac } from "resource://activity-stream/common/Actions.mjs"; import { MESSAGING_EXPERIMENTS_DEFAULT_FEATURES } from "resource:///modules/asrouter/MessagingExperimentConstants.sys.mjs"; import { CFRMessageProvider } from "resource:///modules/asrouter/CFRMessageProvider.sys.mjs"; import { OnboardingMessageProvider } from "resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs"; @@ -620,7 +619,6 @@ export class _ASRouter { this._onLocaleChanged = this._onLocaleChanged.bind(this); this.isUnblockedMessage = this.isUnblockedMessage.bind(this); this.unblockAll = this.unblockAll.bind(this); - this.forceWNPanel = this.forceWNPanel.bind(this); this._onExperimentEnrollmentsUpdated = this._onExperimentEnrollmentsUpdated.bind(this); this.forcePBWindow = this.forcePBWindow.bind(this); @@ -995,10 +993,6 @@ export class _ASRouter { unblockMessageById: this.unblockMessageById, sendTelemetry: this.sendTelemetry, }); - lazy.ToolbarPanelHub.init(this.waitForInitialized, { - getMessages: this.handleMessageRequest, - sendTelemetry: this.sendTelemetry, - }); lazy.MomentsPageHub.init(this.waitForInitialized, { handleMessageRequest: this.handleMessageRequest, addImpression: this.addImpression, @@ -1055,7 +1049,6 @@ export class _ASRouter { lazy.ASRouterPreferences.removeListener(this.onPrefChange); lazy.ASRouterPreferences.uninit(); - lazy.ToolbarPanelHub.uninit(); lazy.ToolbarBadgeHub.uninit(); lazy.MomentsPageHub.uninit(); @@ -1309,16 +1302,6 @@ export class _ASRouter { return true; } - async _extraTemplateStrings(originalMessage) { - let extraTemplateStrings; - let localProvider = this._findProvider(originalMessage.provider); - if (localProvider && localProvider.getExtraAttributes) { - extraTemplateStrings = await localProvider.getExtraAttributes(); - } - - return extraTemplateStrings; - } - _findProvider(providerID) { return this._localProviders[ this.state.providers.find(i => i.id === providerID).localProvider @@ -1346,11 +1329,6 @@ export class _ASRouter { } switch (message.template) { - case "whatsnew_panel_message": - if (force) { - lazy.ToolbarPanelHub.forceShowMessage(browser, message); - } - break; case "cfr_doorhanger": case "milestone_message": if (force) { @@ -2005,29 +1983,6 @@ export class _ASRouter { ); } - async forceWNPanel(browser) { - let win = browser.ownerGlobal; - await lazy.ToolbarPanelHub.enableToolbarButton(); - - win.PanelUI.showSubView( - "PanelUI-whatsNew", - win.document.getElementById("whats-new-menu-button") - ); - - let panel = win.document.getElementById("customizationui-widget-panel"); - // Set the attribute to keep the panel open - panel.setAttribute("noautohide", true); - } - - async closeWNPanel(browser) { - let win = browser.ownerGlobal; - let panel = win.document.getElementById("customizationui-widget-panel"); - // Set the attribute to allow the panel to close - panel.setAttribute("noautohide", false); - // Removing the button is enough to close the panel. - await lazy.ToolbarPanelHub._hideToolbarButton(win); - } - async _onExperimentEnrollmentsUpdated() { const experimentProvider = this.state.providers.find( p => p.id === "messaging-experiments" diff --git a/browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs b/browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs index c2f5fcd884..8aa4d7dbc9 100644 --- a/browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs +++ b/browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs @@ -4,7 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { ASRouterPreferences } from "resource:///modules/asrouter/ASRouterPreferences.sys.mjs"; -import { MESSAGE_TYPE_HASH as msg } from "resource:///modules/asrouter/ActorConstants.sys.mjs"; +import { MESSAGE_TYPE_HASH as msg } from "resource:///modules/asrouter/ActorConstants.mjs"; export class ASRouterParentProcessMessageHandler { constructor({ @@ -27,7 +27,6 @@ export class ASRouterParentProcessMessageHandler { switch (type) { case msg.INFOBAR_TELEMETRY: case msg.TOOLBAR_BADGE_TELEMETRY: - case msg.TOOLBAR_PANEL_TELEMETRY: case msg.MOMENTS_PAGE_TELEMETRY: case msg.DOORHANGER_TELEMETRY: case msg.SPOTLIGHT_TELEMETRY: @@ -128,12 +127,6 @@ export class ASRouterParentProcessMessageHandler { case msg.FORCE_PRIVATE_BROWSING_WINDOW: { return this._router.forcePBWindow(browser, data.message); } - case msg.FORCE_WHATSNEW_PANEL: { - return this._router.forceWNPanel(browser); - } - case msg.CLOSE_WHATSNEW_PANEL: { - return this._router.closeWNPanel(browser); - } case msg.MODIFY_MESSAGE_JSON: { return this._router.routeCFRMessage(data.content, browser, data, true); } diff --git a/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs b/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs index d76b303fc6..9773eda270 100644 --- a/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs +++ b/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs @@ -74,12 +74,6 @@ XPCOMUtils.defineLazyPreferenceGetter( ); XPCOMUtils.defineLazyPreferenceGetter( lazy, - "isWhatsNewPanelEnabled", - "browser.messaging-system.whatsNewPanel.enabled", - false -); -XPCOMUtils.defineLazyPreferenceGetter( - lazy, "hasAccessedFxAPanel", "identity.fxaccounts.toolbar.accessed", false @@ -704,9 +698,6 @@ const TargetingGetters = { get hasAccessedFxAPanel() { return lazy.hasAccessedFxAPanel; }, - get isWhatsNewPanelEnabled() { - return lazy.isWhatsNewPanelEnabled; - }, get userPrefs() { return { cfrFeatures: lazy.cfrFeaturesUserPref, diff --git a/browser/components/asrouter/modules/ActorConstants.sys.mjs b/browser/components/asrouter/modules/ActorConstants.mjs index 4c996552ab..c1c18e006e 100644 --- a/browser/components/asrouter/modules/ActorConstants.sys.mjs +++ b/browser/components/asrouter/modules/ActorConstants.mjs @@ -12,7 +12,6 @@ export const MESSAGE_TYPE_LIST = [ "PBNEWTAB_MESSAGE_REQUEST", "DOORHANGER_TELEMETRY", "TOOLBAR_BADGE_TELEMETRY", - "TOOLBAR_PANEL_TELEMETRY", "MOMENTS_PAGE_TELEMETRY", "INFOBAR_TELEMETRY", "SPOTLIGHT_TELEMETRY", @@ -30,9 +29,7 @@ export const MESSAGE_TYPE_LIST = [ "EVALUATE_JEXL_EXPRESSION", "EXPIRE_QUERY_CACHE", "FORCE_ATTRIBUTION", - "FORCE_WHATSNEW_PANEL", "FORCE_PRIVATE_BROWSING_WINDOW", - "CLOSE_WHATSNEW_PANEL", "OVERRIDE_MESSAGE", "MODIFY_MESSAGE_JSON", "RESET_PROVIDER_PREF", diff --git a/browser/components/asrouter/modules/MomentsPageHub.sys.mjs b/browser/components/asrouter/modules/MomentsPageHub.sys.mjs index 84fee3b517..3a59e9d450 100644 --- a/browser/components/asrouter/modules/MomentsPageHub.sys.mjs +++ b/browser/components/asrouter/modules/MomentsPageHub.sys.mjs @@ -165,7 +165,7 @@ export class _MomentsPageHub { } /** - * ToolbarBadgeHub - singleton instance of _ToolbarBadgeHub that can initiate + * MomentsPageHub - singleton instance of _MomentsPageHub that can initiate * message requests and render messages. */ export const MomentsPageHub = new _MomentsPageHub(); diff --git a/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs b/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs index ceded6b755..3cfbbb3f34 100644 --- a/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs +++ b/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs @@ -1210,6 +1210,106 @@ const BASE_MESSAGES = () => [ id: "defaultBrowserCheck", }, }, + { + id: "SET_DEFAULT_BROWSER_GUIDANCE_NOTIFICATION_WIN10", + template: "toast_notification", + content: { + title: { + string_id: "default-browser-guidance-notification-title", + }, + body: { + string_id: + "default-browser-guidance-notification-body-instruction-win10", + }, + launch_action: { + type: "OPEN_URL", + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/win-set-firefox-default-browser", + where: "tabshifted", + }, + }, + requireInteraction: true, + actions: [ + { + action: "info-page", + title: { + string_id: "default-browser-guidance-notification-info-page", + }, + launch_action: { + type: "OPEN_URL", + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/win-set-firefox-default-browser", + where: "tabshifted", + }, + }, + }, + { + action: "dismiss", + title: { + string_id: "default-browser-guidance-notification-dismiss", + }, + windowsSystemActivationType: true, + }, + ], + tag: "set-default-guidance-notification", + }, + // Both Windows 10 and 11 return `os.windowsVersion == 10.0`. We limit to + // only Windows 10 with `os.windowsBuildNumber < 22000`. We need this due to + // Windows 10 and 11 having substantively different UX for Windows Settings. + targeting: + "os.isWindows && os.windowsVersion >= 10.0 && os.windowsBuildNumber < 22000", + trigger: { id: "deeplinkedToWindowsSettingsUI" }, + }, + { + id: "SET_DEFAULT_BROWSER_GUIDANCE_NOTIFICATION_WIN11", + template: "toast_notification", + content: { + title: { + string_id: "default-browser-guidance-notification-title", + }, + body: { + string_id: + "default-browser-guidance-notification-body-instruction-win11", + }, + launch_action: { + type: "OPEN_URL", + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/win-set-firefox-default-browser", + where: "tabshifted", + }, + }, + requireInteraction: true, + actions: [ + { + action: "info-page", + title: { + string_id: "default-browser-guidance-notification-info-page", + }, + launch_action: { + type: "OPEN_URL", + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/win-set-firefox-default-browser", + where: "tabshifted", + }, + }, + }, + { + action: "dismiss", + title: { + string_id: "default-browser-guidance-notification-dismiss", + }, + windowsSystemActivationType: true, + }, + ], + tag: "set-default-guidance-notification", + }, + // Both Windows 10 and 11 return `os.windowsVersion == 10.0`. We limit to + // only Windows 11 with `os.windowsBuildNumber >= 22000`. We need this due to + // Windows 10 and 11 having substantively different UX for Windows Settings. + targeting: + "os.isWindows && os.windowsVersion >= 10.0 && os.windowsBuildNumber >= 22000", + trigger: { id: "deeplinkedToWindowsSettingsUI" }, + }, ]; // Eventually, move Feature Callout messages to their own provider diff --git a/browser/components/asrouter/modules/PanelTestProvider.sys.mjs b/browser/components/asrouter/modules/PanelTestProvider.sys.mjs index 7a7ff1e1fc..5180e2e6a2 100644 --- a/browser/components/asrouter/modules/PanelTestProvider.sys.mjs +++ b/browser/components/asrouter/modules/PanelTestProvider.sys.mjs @@ -20,134 +20,6 @@ const MESSAGES = () => [ trigger: { id: "momentsUpdate" }, }, { - id: "WHATS_NEW_FINGERPRINTER_COUNTER_ALT", - template: "whatsnew_panel_message", - order: 6, - content: { - bucket_id: "WHATS_NEW_72", - published_date: 1574776601000, - title: "Title", - icon_url: - "chrome://activity-stream/content/data/content/assets/protection-report-icon.png", - icon_alt: { string_id: "cfr-badge-reader-label-newfeature" }, - body: "Message body", - link_text: "Click here", - cta_url: "about:blank", - cta_type: "OPEN_PROTECTION_REPORT", - }, - targeting: `firefoxVersion >= 72`, - trigger: { id: "whatsNewPanelOpened" }, - }, - { - id: "WHATS_NEW_70_1", - template: "whatsnew_panel_message", - order: 3, - content: { - bucket_id: "WHATS_NEW_70_1", - published_date: 1560969794394, - title: "Protection Is Our Focus", - icon_url: - "chrome://activity-stream/content/data/content/assets/whatsnew-send-icon.png", - icon_alt: "Firefox Send Logo", - body: "The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.", - cta_url: "https://blog.mozilla.org/", - cta_type: "OPEN_URL", - }, - targeting: `firefoxVersion > 69`, - trigger: { id: "whatsNewPanelOpened" }, - }, - { - id: "WHATS_NEW_70_2", - template: "whatsnew_panel_message", - order: 1, - content: { - bucket_id: "WHATS_NEW_70_1", - published_date: 1560969794394, - title: "Another thing new in Firefox 70", - body: "The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.", - link_text: "Learn more on our blog", - cta_url: "https://blog.mozilla.org/", - cta_type: "OPEN_URL", - }, - targeting: `firefoxVersion > 69`, - trigger: { id: "whatsNewPanelOpened" }, - }, - { - id: "WHATS_NEW_SEARCH_SHORTCUTS_84", - template: "whatsnew_panel_message", - order: 2, - content: { - bucket_id: "WHATS_NEW_SEARCH_SHORTCUTS_84", - published_date: 1560969794394, - title: "Title", - icon_url: "chrome://global/skin/icons/check.svg", - icon_alt: "", - body: "Message content", - cta_url: - "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/search-shortcuts", - cta_type: "OPEN_URL", - link_text: "Click here", - }, - targeting: "firefoxVersion >= 84", - trigger: { - id: "whatsNewPanelOpened", - }, - }, - { - id: "WHATS_NEW_PIONEER_82", - template: "whatsnew_panel_message", - order: 1, - content: { - bucket_id: "WHATS_NEW_PIONEER_82", - published_date: 1603152000000, - title: "Put your data to work for a better internet", - body: "Contribute your data to Mozilla's Pioneer program to help researchers understand pressing technology issues like misinformation, data privacy, and ethical AI.", - cta_url: "about:blank", - cta_where: "tab", - cta_type: "OPEN_ABOUT_PAGE", - link_text: "Join Pioneer", - }, - targeting: "firefoxVersion >= 82", - trigger: { - id: "whatsNewPanelOpened", - }, - }, - { - id: "WHATS_NEW_MEDIA_SESSION_82", - template: "whatsnew_panel_message", - order: 3, - content: { - bucket_id: "WHATS_NEW_MEDIA_SESSION_82", - published_date: 1603152000000, - title: "Title", - body: "Message content", - cta_url: - "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/media-keyboard-control", - cta_type: "OPEN_URL", - link_text: "Click here", - }, - targeting: "firefoxVersion >= 82", - trigger: { - id: "whatsNewPanelOpened", - }, - }, - { - id: "WHATS_NEW_69_1", - template: "whatsnew_panel_message", - order: 1, - content: { - bucket_id: "WHATS_NEW_69_1", - published_date: 1557346235089, - title: "Something new in Firefox 69", - body: "The New Enhanced Tracking Protection, gives you the best level of protection and performance. Discover how this version is the safest version of firefox ever made.", - link_text: "Learn more on our blog", - cta_url: "https://blog.mozilla.org/", - cta_type: "OPEN_URL", - }, - targeting: `firefoxVersion > 68`, - trigger: { id: "whatsNewPanelOpened" }, - }, - { id: "PERSONALIZED_CFR_MESSAGE", template: "cfr_doorhanger", groups: ["cfr"], diff --git a/browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs b/browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs index 57fd104f19..36f7ca5005 100644 --- a/browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs +++ b/browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs @@ -10,7 +10,6 @@ ChromeUtils.defineESModuleGetters(lazy, { clearTimeout: "resource://gre/modules/Timer.sys.mjs", requestIdleCallback: "resource://gre/modules/Timer.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", - ToolbarPanelHub: "resource:///modules/asrouter/ToolbarPanelHub.sys.mjs", }); let notificationsByWindow = new WeakMap(); @@ -19,9 +18,6 @@ export class _ToolbarBadgeHub { constructor() { this.id = "toolbar-badge-hub"; this.state = {}; - this.prefs = { - WHATSNEW_TOOLBAR_PANEL: "browser.messaging-system.whatsNewPanel.enabled", - }; this.removeAllNotifications = this.removeAllNotifications.bind(this); this.removeToolbarNotification = this.removeToolbarNotification.bind(this); this.addToolbarNotification = this.addToolbarNotification.bind(this); @@ -62,34 +58,12 @@ export class _ToolbarBadgeHub { triggerId: "toolbarBadgeUpdate", template: "toolbar_badge", }); - // Listen for pref changes that could trigger new badges - Services.prefs.addObserver(this.prefs.WHATSNEW_TOOLBAR_PANEL, this); - } - - observe(aSubject, aTopic, aPrefName) { - switch (aPrefName) { - case this.prefs.WHATSNEW_TOOLBAR_PANEL: - this.messageRequest({ - triggerId: "toolbarBadgeUpdate", - template: "toolbar_badge", - }); - break; - } } maybeInsertFTL(win) { win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl"); } - executeAction({ id }) { - switch (id) { - case "show-whatsnew-button": - lazy.ToolbarPanelHub.enableToolbarButton(); - lazy.ToolbarPanelHub.enableAppmenuButton(); - break; - } - } - _clearBadgeTimeout() { if (this.state.showBadgeTimeoutId) { lazy.clearTimeout(this.state.showBadgeTimeoutId); @@ -153,9 +127,6 @@ export class _ToolbarBadgeHub { addToolbarNotification(win, message) { const document = win.browser.ownerDocument; - if (message.content.action) { - this.executeAction({ ...message.content.action, message_id: message.id }); - } let toolbarbutton = document.getElementById(message.content.target); if (toolbarbutton) { const badge = toolbarbutton.querySelector(".toolbarbutton-badge"); @@ -211,12 +182,6 @@ export class _ToolbarBadgeHub { } registerBadgeToAllWindows(message) { - if (message.template === "update_action") { - this.executeAction({ ...message.content.action, message_id: message.id }); - // No badge to set only an action to execute - return; - } - lazy.EveryWindow.registerCallback( this.id, win => { @@ -297,7 +262,6 @@ export class _ToolbarBadgeHub { this.state = {}; this._initialized = false; notificationsByWindow = new WeakMap(); - Services.prefs.removeObserver(this.prefs.WHATSNEW_TOOLBAR_PANEL, this); } } diff --git a/browser/components/asrouter/modules/ToolbarPanelHub.sys.mjs b/browser/components/asrouter/modules/ToolbarPanelHub.sys.mjs deleted file mode 100644 index 519bca8a89..0000000000 --- a/browser/components/asrouter/modules/ToolbarPanelHub.sys.mjs +++ /dev/null @@ -1,544 +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/. */ - -const lazy = {}; - -// We use importESModule here instead of static import so that -// the Karma test environment won't choke on this module. This -// is because the Karma test environment already stubs out -// XPCOMUtils. That environment overrides importESModule to be a no-op -// (which can't be done for a static import statement). - -// eslint-disable-next-line mozilla/use-static-import -const { XPCOMUtils } = ChromeUtils.importESModule( - "resource://gre/modules/XPCOMUtils.sys.mjs" -); - -ChromeUtils.defineESModuleGetters(lazy, { - EveryWindow: "resource:///modules/EveryWindow.sys.mjs", - PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs", - PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", - SpecialMessageActions: - "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", - RemoteL10n: "resource:///modules/asrouter/RemoteL10n.sys.mjs", -}); - -XPCOMUtils.defineLazyServiceGetter( - lazy, - "TrackingDBService", - "@mozilla.org/tracking-db-service;1", - "nsITrackingDBService" -); - -const idToTextMap = new Map([ - [Ci.nsITrackingDBService.TRACKERS_ID, "trackerCount"], - [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookieCount"], - [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominerCount"], - [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinterCount"], - [Ci.nsITrackingDBService.SOCIAL_ID, "socialCount"], -]); - -const WHATSNEW_ENABLED_PREF = "browser.messaging-system.whatsNewPanel.enabled"; -const PROTECTIONS_PANEL_INFOMSG_PREF = - "browser.protections_panel.infoMessage.seen"; - -const TOOLBAR_BUTTON_ID = "whats-new-menu-button"; -const APPMENU_BUTTON_ID = "appMenu-whatsnew-button"; - -const BUTTON_STRING_ID = "cfr-whatsnew-button"; -const WHATS_NEW_PANEL_SELECTOR = "PanelUI-whatsNew-message-container"; - -export class _ToolbarPanelHub { - constructor() { - this.triggerId = "whatsNewPanelOpened"; - this._showAppmenuButton = this._showAppmenuButton.bind(this); - this._hideAppmenuButton = this._hideAppmenuButton.bind(this); - this._showToolbarButton = this._showToolbarButton.bind(this); - this._hideToolbarButton = this._hideToolbarButton.bind(this); - - this.state = {}; - this._initialized = false; - } - - async init(waitForInitialized, { getMessages, sendTelemetry }) { - if (this._initialized) { - return; - } - - this._initialized = true; - this._getMessages = getMessages; - this._sendTelemetry = sendTelemetry; - // Wait for ASRouter messages to become available in order to know - // if we can show the What's New panel - await waitForInitialized; - // Enable the application menu button so that the user can access - // the panel outside of the toolbar button - await this.enableAppmenuButton(); - - this.state = { - protectionPanelMessageSeen: Services.prefs.getBoolPref( - PROTECTIONS_PANEL_INFOMSG_PREF, - false - ), - }; - } - - uninit() { - this._initialized = false; - lazy.EveryWindow.unregisterCallback(TOOLBAR_BUTTON_ID); - lazy.EveryWindow.unregisterCallback(APPMENU_BUTTON_ID); - } - - get messages() { - return this._getMessages({ - template: "whatsnew_panel_message", - triggerId: "whatsNewPanelOpened", - returnAll: true, - }); - } - - toggleWhatsNewPref(event) { - // Checkbox onclick handler gets called before the checkbox state gets toggled, - // so we have to call it with the opposite value. - let newValue = !event.target.checked; - Services.prefs.setBoolPref(WHATSNEW_ENABLED_PREF, newValue); - - this.sendUserEventTelemetry( - event.target.ownerGlobal, - "WNP_PREF_TOGGLE", - // Message id is not applicable in this case, the notification state - // is not related to a particular message - { id: "n/a" }, - { value: { prefValue: newValue } } - ); - } - - maybeInsertFTL(win) { - win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl"); - win.MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl"); - win.MozXULElement.insertFTLIfNeeded("toolkit/branding/accounts.ftl"); - } - - maybeLoadCustomElement(win) { - if (!win.customElements.get("remote-text")) { - Services.scriptloader.loadSubScript( - "resource://activity-stream/data/custom-elements/paragraph.js", - win - ); - } - } - - // Turns on the Appmenu (hamburger menu) button for all open windows and future windows. - async enableAppmenuButton() { - if ((await this.messages).length) { - lazy.EveryWindow.registerCallback( - APPMENU_BUTTON_ID, - this._showAppmenuButton, - this._hideAppmenuButton - ); - } - } - - // Removes the button from the Appmenu. - // Only used in tests. - disableAppmenuButton() { - lazy.EveryWindow.unregisterCallback(APPMENU_BUTTON_ID); - } - - // Turns on the Toolbar button for all open windows and future windows. - async enableToolbarButton() { - if ((await this.messages).length) { - lazy.EveryWindow.registerCallback( - TOOLBAR_BUTTON_ID, - this._showToolbarButton, - this._hideToolbarButton - ); - } - } - - // When the panel is hidden we want to run some cleanup - _onPanelHidden(win) { - const panelContainer = win.document.getElementById( - "customizationui-widget-panel" - ); - // When the panel is hidden we want to remove any toolbar buttons that - // might have been added as an entry point to the panel - const removeToolbarButton = () => { - lazy.EveryWindow.unregisterCallback(TOOLBAR_BUTTON_ID); - }; - if (!panelContainer) { - return; - } - panelContainer.addEventListener("popuphidden", removeToolbarButton, { - once: true, - }); - } - - // Newer messages first and use `order` field to decide between messages - // with the same timestamp - _sortWhatsNewMessages(m1, m2) { - // Sort by published_date in descending order. - if (m1.content.published_date === m2.content.published_date) { - // Ascending order - return m1.order - m2.order; - } - if (m1.content.published_date > m2.content.published_date) { - return -1; - } - return 1; - } - - // Render what's new messages into the panel. - async renderMessages(win, doc, containerId, options = {}) { - // Set the checked status of the footer checkbox - let value = Services.prefs.getBoolPref(WHATSNEW_ENABLED_PREF); - let checkbox = win.document.getElementById("panelMenu-toggleWhatsNew"); - - checkbox.checked = value; - - this.maybeLoadCustomElement(win); - const messages = - (options.force && options.messages) || - (await this.messages).sort(this._sortWhatsNewMessages); - const container = lazy.PanelMultiView.getViewNode(doc, containerId); - - if (messages) { - // Targeting attribute state might have changed making new messages - // available and old messages invalid, we need to refresh - this.removeMessages(win, containerId); - let previousDate = 0; - // Get and store any variable part of the message content - this.state.contentArguments = await this._contentArguments(); - for (let message of messages) { - container.appendChild( - this._createMessageElements(win, doc, message, previousDate) - ); - previousDate = message.content.published_date; - } - } - - this._onPanelHidden(win); - - // Panel impressions are not associated with one particular message - // but with a set of messages. We concatenate message ids and send them - // back for every impression. - const eventId = { - id: messages - .map(({ id }) => id) - .sort() - .join(","), - }; - // Check `mainview` attribute to determine if the panel is shown as a - // subview (inside the application menu) or as a toolbar dropdown. - // https://searchfox.org/mozilla-central/rev/07f7390618692fa4f2a674a96b9b677df3a13450/browser/components/customizableui/PanelMultiView.jsm#1268 - const mainview = win.PanelUI.whatsNewPanel.hasAttribute("mainview"); - this.sendUserEventTelemetry(win, "IMPRESSION", eventId, { - value: { view: mainview ? "toolbar_dropdown" : "application_menu" }, - }); - } - - removeMessages(win, containerId) { - const doc = win.document; - const messageNodes = lazy.PanelMultiView.getViewNode( - doc, - containerId - ).querySelectorAll(".whatsNew-message"); - for (const messageNode of messageNodes) { - messageNode.remove(); - } - } - - /** - * Dispatch the action defined in the message and user telemetry event. - */ - _dispatchUserAction(win, message) { - let url; - try { - // Set platform specific path variables for SUMO articles - url = Services.urlFormatter.formatURL(message.content.cta_url); - } catch (e) { - console.error(e); - url = message.content.cta_url; - } - lazy.SpecialMessageActions.handleAction( - { - type: message.content.cta_type, - data: { - args: url, - where: message.content.cta_where || "tabshifted", - }, - }, - win.browser - ); - - this.sendUserEventTelemetry(win, "CLICK", message); - } - - /** - * Attach event listener to dispatch message defined action. - */ - _attachCommandListener(win, element, message) { - // Add event listener for `mouseup` not to overlap with the - // `mousedown` & `click` events dispatched from PanelMultiView.sys.mjs - // https://searchfox.org/mozilla-central/rev/7531325c8660cfa61bf71725f83501028178cbb9/browser/components/customizableui/PanelMultiView.jsm#1830-1837 - element.addEventListener("mouseup", () => { - this._dispatchUserAction(win, message); - }); - element.addEventListener("keyup", e => { - if (e.key === "Enter" || e.key === " ") { - this._dispatchUserAction(win, message); - } - }); - } - - _createMessageElements(win, doc, message, previousDate) { - const { content } = message; - const messageEl = lazy.RemoteL10n.createElement(doc, "div"); - messageEl.classList.add("whatsNew-message"); - - // Only render date if it is different from the one rendered before. - if (content.published_date !== previousDate) { - messageEl.appendChild( - lazy.RemoteL10n.createElement(doc, "p", { - classList: "whatsNew-message-date", - content: new Date(content.published_date).toLocaleDateString( - "default", - { - month: "long", - day: "numeric", - year: "numeric", - } - ), - }) - ); - } - - const wrapperEl = lazy.RemoteL10n.createElement(doc, "div"); - wrapperEl.doCommand = () => this._dispatchUserAction(win, message); - wrapperEl.classList.add("whatsNew-message-body"); - messageEl.appendChild(wrapperEl); - - if (content.icon_url) { - wrapperEl.classList.add("has-icon"); - const iconEl = lazy.RemoteL10n.createElement(doc, "img"); - iconEl.src = content.icon_url; - iconEl.classList.add("whatsNew-message-icon"); - if (content.icon_alt && content.icon_alt.string_id) { - doc.l10n.setAttributes(iconEl, content.icon_alt.string_id); - } else { - iconEl.setAttribute("alt", content.icon_alt); - } - wrapperEl.appendChild(iconEl); - } - - wrapperEl.appendChild(this._createMessageContent(win, doc, content)); - - if (content.link_text) { - const anchorEl = lazy.RemoteL10n.createElement(doc, "a", { - classList: "text-link", - content: content.link_text, - }); - anchorEl.doCommand = () => this._dispatchUserAction(win, message); - wrapperEl.appendChild(anchorEl); - } - - // Attach event listener on entire message container - this._attachCommandListener(win, messageEl, message); - - return messageEl; - } - - /** - * Return message title (optional subtitle) and body - */ - _createMessageContent(win, doc, content) { - const wrapperEl = new win.DocumentFragment(); - - wrapperEl.appendChild( - lazy.RemoteL10n.createElement(doc, "h2", { - classList: "whatsNew-message-title", - content: content.title, - attributes: this.state.contentArguments, - }) - ); - - wrapperEl.appendChild( - lazy.RemoteL10n.createElement(doc, "p", { - content: content.body, - classList: "whatsNew-message-content", - attributes: this.state.contentArguments, - }) - ); - - return wrapperEl; - } - - _createHeroElement(win, doc, message) { - this.maybeLoadCustomElement(win); - - const messageEl = lazy.RemoteL10n.createElement(doc, "div"); - messageEl.setAttribute("id", "protections-popup-message"); - messageEl.classList.add("whatsNew-hero-message"); - const wrapperEl = lazy.RemoteL10n.createElement(doc, "div"); - wrapperEl.classList.add("whatsNew-message-body"); - messageEl.appendChild(wrapperEl); - - wrapperEl.appendChild( - lazy.RemoteL10n.createElement(doc, "h2", { - classList: "whatsNew-message-title", - content: message.content.title, - }) - ); - wrapperEl.appendChild( - lazy.RemoteL10n.createElement(doc, "p", { - classList: "protections-popup-content", - content: message.content.body, - }) - ); - - if (message.content.link_text) { - let linkEl = lazy.RemoteL10n.createElement(doc, "a", { - classList: "text-link", - content: message.content.link_text, - }); - linkEl.disabled = true; - wrapperEl.appendChild(linkEl); - this._attachCommandListener(win, linkEl, message); - } else { - this._attachCommandListener(win, wrapperEl, message); - } - - return messageEl; - } - - async _contentArguments() { - const { defaultEngine } = Services.search; - // Between now and 6 weeks ago - const dateTo = new Date(); - const dateFrom = new Date(dateTo.getTime() - 42 * 24 * 60 * 60 * 1000); - const eventsByDate = await lazy.TrackingDBService.getEventsByDateRange( - dateFrom, - dateTo - ); - // Make sure we set all types of possible values to 0 because they might - // be referenced by fluent strings - let totalEvents = { blockedCount: 0 }; - for (let blockedType of idToTextMap.values()) { - totalEvents[blockedType] = 0; - } - // Count all events in the past 6 weeks. Returns an object with: - // `blockedCount` total number of blocked resources - // {tracker|cookie|social...} breakdown by event type as defined by `idToTextMap` - totalEvents = eventsByDate.reduce((acc, day) => { - const type = day.getResultByName("type"); - const count = day.getResultByName("count"); - acc[idToTextMap.get(type)] = (acc[idToTextMap.get(type)] || 0) + count; - acc.blockedCount += count; - return acc; - }, totalEvents); - return { - // Keys need to match variable names used in asrouter.ftl - // `earliestDate` will be either 6 weeks ago or when tracking recording - // started. Whichever is more recent. - earliestDate: Math.max( - new Date(await lazy.TrackingDBService.getEarliestRecordedDate()), - dateFrom - ), - ...totalEvents, - // Passing in `undefined` as string for the Fluent variable name - // in order to match and select the message that does not require - // the variable. - searchEngineName: defaultEngine ? defaultEngine.name : "undefined", - }; - } - - async _showAppmenuButton(win) { - this.maybeInsertFTL(win); - await this._showElement( - win.browser.ownerDocument, - APPMENU_BUTTON_ID, - BUTTON_STRING_ID - ); - } - - _hideAppmenuButton(win, windowClosed) { - // No need to do something if the window is going away - if (!windowClosed) { - this._hideElement(win.browser.ownerDocument, APPMENU_BUTTON_ID); - } - } - - _showToolbarButton(win) { - const document = win.browser.ownerDocument; - this.maybeInsertFTL(win); - return this._showElement(document, TOOLBAR_BUTTON_ID, BUTTON_STRING_ID); - } - - _hideToolbarButton(win) { - this._hideElement(win.browser.ownerDocument, TOOLBAR_BUTTON_ID); - } - - _showElement(document, id, string_id) { - const el = lazy.PanelMultiView.getViewNode(document, id); - document.l10n.setAttributes(el, string_id); - el.hidden = false; - } - - _hideElement(document, id) { - const el = lazy.PanelMultiView.getViewNode(document, id); - if (el) { - el.hidden = true; - } - } - - _sendPing(ping) { - this._sendTelemetry({ - type: "TOOLBAR_PANEL_TELEMETRY", - data: { action: "whats-new-panel_user_event", ...ping }, - }); - } - - sendUserEventTelemetry(win, event, message, options = {}) { - // Only send pings for non private browsing windows - if ( - win && - !lazy.PrivateBrowsingUtils.isBrowserPrivate( - win.ownerGlobal.gBrowser.selectedBrowser - ) - ) { - this._sendPing({ - message_id: message.id, - event, - event_context: options.value, - }); - } - } - - /** - * @param {object} [browser] MessageChannel target argument as a response to a - * user action. No message is shown if undefined. - * @param {object[]} messages Messages selected from devtools page - */ - forceShowMessage(browser, messages) { - if (!browser) { - return; - } - const win = browser.ownerGlobal; - const doc = browser.ownerDocument; - this.removeMessages(win, WHATS_NEW_PANEL_SELECTOR); - this.renderMessages(win, doc, WHATS_NEW_PANEL_SELECTOR, { - force: true, - messages: Array.isArray(messages) ? messages : [messages], - }); - win.PanelUI.panel.addEventListener("popuphidden", event => - this.removeMessages(event.target.ownerGlobal, WHATS_NEW_PANEL_SELECTOR) - ); - } -} - -/** - * ToolbarPanelHub - singleton instance of _ToolbarPanelHub that can initiate - * message requests and render messages. - */ -export const ToolbarPanelHub = new _ToolbarPanelHub(); diff --git a/browser/components/asrouter/moz.build b/browser/components/asrouter/moz.build index 558ccbeb9b..cf45186619 100644 --- a/browser/components/asrouter/moz.build +++ b/browser/components/asrouter/moz.build @@ -15,7 +15,7 @@ FINAL_TARGET_FILES.actors += [ ] EXTRA_JS_MODULES.asrouter += [ - "modules/ActorConstants.sys.mjs", + "modules/ActorConstants.mjs", "modules/ASRouter.sys.mjs", "modules/ASRouterDefaultConfig.sys.mjs", "modules/ASRouterNewTabHook.sys.mjs", @@ -38,7 +38,6 @@ EXTRA_JS_MODULES.asrouter += [ "modules/Spotlight.sys.mjs", "modules/ToastNotification.sys.mjs", "modules/ToolbarBadgeHub.sys.mjs", - "modules/ToolbarPanelHub.sys.mjs", ] BROWSER_CHROME_MANIFESTS += [ @@ -57,7 +56,6 @@ TESTING_JS_MODULES += [ "content-src/templates/OnboardingMessage/Spotlight.schema.json", "content-src/templates/OnboardingMessage/ToolbarBadgeMessage.schema.json", "content-src/templates/OnboardingMessage/UpdateAction.schema.json", - "content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json", "content-src/templates/PBNewtab/NewtabPromoMessage.schema.json", "content-src/templates/ToastNotification/ToastNotification.schema.json", "tests/InflightAssetsMessageProvider.sys.mjs", diff --git a/browser/components/asrouter/package.json b/browser/components/asrouter/package.json index 17bc8f7364..26c09392c2 100644 --- a/browser/components/asrouter/package.json +++ b/browser/components/asrouter/package.json @@ -60,6 +60,7 @@ "testmc:lint": "npm run lint", "testmc:build": "npm run bundle:admin", "testmc:unit": "karma start karma.mc.config.js", + "testmc:import": "npm run import-rollouts", "tddmc": "karma start karma.mc.config.js --tdd", "debugcoverage": "open logs/coverage/lcov-report/index.html", "lint": "npm-run-all lint:*", diff --git a/browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs b/browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs index e92b210c12..adb14ecc38 100644 --- a/browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs +++ b/browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs @@ -10,58 +10,10 @@ export const InflightAssetsMessageProvider = { getMessages() { return [ { - id: "MILESTONE_MESSAGE", - groups: ["cfr"], - content: { - anchor_id: "tracking-protection-icon-box", - bucket_id: "CFR_MILESTONE_MESSAGE", - buttons: { - primary: { - action: { - type: "OPEN_PROTECTION_REPORT", - }, - event: "PROTECTION", - label: { - string_id: "cfr-doorhanger-milestone-ok-button", - }, - }, - secondary: [ - { - label: { - string_id: "cfr-doorhanger-milestone-close-button", - }, - action: { - type: "CANCEL", - }, - event: "DISMISS", - }, - ], - }, - category: "cfrFeatures", - heading_text: { - string_id: "cfr-doorhanger-milestone-heading", - }, - layout: "short_message", - notification_text: "", - skip_address_bar_notifier: true, - text: "", - }, - frequency: { - lifetime: 7, - }, - targeting: - "pageLoad >= 4 && firefoxVersion < 87 && userPrefs.cfrFeatures", - template: "milestone_message", - trigger: { - id: "contentBlocking", - params: ["ContentBlockingMilestone"], - }, - }, - { id: "MILESTONE_MESSAGE_87", groups: ["cfr"], content: { - anchor_id: "tracking-protection-icon-box", + anchor_id: "tracking-protection-icon-container", bucket_id: "CFR_MILESTONE_MESSAGE", buttons: { primary: { @@ -98,7 +50,7 @@ export const InflightAssetsMessageProvider = { lifetime: 7, }, targeting: - "pageLoad >= 4 && firefoxVersion >= 87 && userPrefs.cfrFeatures", + "pageLoad >= 4 && firefoxVersion >= 115 && firefoxVersion < 121 && userPrefs.cfrFeatures", template: "milestone_message", trigger: { id: "contentBlocking", @@ -275,66 +227,6 @@ export const InflightAssetsMessageProvider = { ], }, }, - { - id: "WNP_MOMENTS_12", - groups: ["moments-pages"], - content: { - action: { - data: { - expire: 1640908800000, - url: "https://www.mozilla.org/firefox/welcome/12", - }, - id: "moments-wnp", - }, - bucket_id: "WNP_MOMENTS_12", - }, - targeting: - 'localeLanguageCode == "en" && region in ["DE", "AT", "BE", "CA", "FR", "IE", "IT", "MY", "NL", "NZ", "SG", "CH", "US", "GB", "ES"] && (addonsInfo.addons|keys intersect ["@testpilot-containers"])|length == 1 && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\'|preferenceValue && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\'|preferenceValue', - template: "update_action", - trigger: { - id: "momentsUpdate", - }, - }, - { - id: "WNP_MOMENTS_13", - groups: ["moments-pages"], - content: { - action: { - data: { - expire: 1640908800000, - url: "https://www.mozilla.org/firefox/welcome/13", - }, - id: "moments-wnp", - }, - bucket_id: "WNP_MOMENTS_13", - }, - targeting: - '(localeLanguageCode in ["en", "de", "fr", "nl", "it", "ms"] || locale == "es-ES") && region in ["DE", "AT", "BE", "CA", "FR", "IE", "IT", "MY", "NL", "NZ", "SG", "CH", "US", "GB", "ES"] && (addonsInfo.addons|keys intersect ["@testpilot-containers"])|length == 0 && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\'|preferenceValue && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\'|preferenceValue', - template: "update_action", - trigger: { - id: "momentsUpdate", - }, - }, - { - id: "WNP_MOMENTS_14", - groups: ["moments-pages"], - content: { - action: { - data: { - expire: 1668470400000, - url: "https://www.mozilla.org/firefox/welcome/14", - }, - id: "moments-wnp", - }, - bucket_id: "WNP_MOMENTS_14", - }, - targeting: - 'localeLanguageCode in ["en", "de", "fr"] && region in ["AT", "BE", "CA", "CH", "DE", "ES", "FI", "FR", "GB", "IE", "IT", "MY", "NL", "NZ", "SE", "SG", "US"] && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\'|preferenceValue && \'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\'|preferenceValue', - template: "update_action", - trigger: { - id: "momentsUpdate", - }, - }, ]; }, }; diff --git a/browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs b/browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs index 5bfbec9557..f2c94f7de6 100644 --- a/browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs +++ b/browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs @@ -12,6 +12,427 @@ export const NimbusRolloutMessageProvider = { getMessages() { return [ { + // Nimbus slug: device-migration-q4-spotlights-remaining-population-esr:treatment (message 1 of 3) + // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population-esr/summary#treatment + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + groups: ["eco"], + content: { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + modal: "tab", + screens: [ + { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_BACKUP", + content: { + logo: { + height: "152px", + imageURL: + "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/c92a41e4-82cf-4ad5-8480-04a138bfb3cd.png", + }, + title: { + fontSize: "24px", + string_id: "device-migration-fxa-spotlight-heavy-user-header", + letterSpacing: 0, + }, + subtitle: { + fontSize: "15px", + string_id: "device-migration-fxa-spotlight-heavy-user-body", + lineHeight: "1.4", + marginBlock: "8px 20px", + letterSpacing: 0, + paddingInline: "20px", + }, + dismiss_button: { + action: { + navigate: true, + }, + }, + primary_button: { + label: { + string_id: + "device-migration-fxa-spotlight-heavy-user-primary-button", + paddingBlock: "4px", + paddingInline: "16px", + }, + action: { + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=dont-forget-to-backup&entrypoint=device-migration-spotlight-experiment-v2", + where: "tabshifted", + }, + type: "OPEN_URL", + navigate: true, + }, + }, + }, + }, + ], + backdrop: "transparent", + template: "multistage", + transitions: true, + }, + trigger: { + id: "defaultBrowserCheck", + }, + template: "spotlight", + frequency: { + lifetime: 1, + }, + targeting: + "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && (((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35)", + }, + { + // Nimbus slug: device-migration-q4-spotlights-remaining-population-esr:treatment (message 2 of 3) + // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population-esr/summary#treatment + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + groups: ["eco"], + content: { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + modal: "tab", + screens: [ + { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_PEACE", + content: { + logo: { + height: "133px", + imageURL: + "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/4a56d3ed-98c8-4a33-b853-b2cf7646efd8.png", + marginBlock: "22px -10px", + }, + title: { + fontSize: "24px", + string_id: + "device-migration-fxa-spotlight-older-device-header", + letterSpacing: 0, + }, + subtitle: { + fontSize: "15px", + string_id: "device-migration-fxa-spotlight-older-device-body", + lineHeight: "1.4", + marginBlock: "8px 20px", + letterSpacing: 0, + paddingInline: "40px", + }, + dismiss_button: { + action: { + navigate: true, + }, + }, + primary_button: { + label: { + string_id: + "device-migration-fxa-spotlight-older-device-primary-button", + marginBlock: "0 22px", + paddingBlock: "4px", + paddingInline: "16px", + }, + action: { + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=peace-of-mind&entrypoint=device-migration-spotlight-experiment-v2", + where: "tabshifted", + }, + type: "OPEN_URL", + navigate: true, + }, + }, + }, + }, + ], + backdrop: "transparent", + template: "multistage", + transitions: true, + }, + trigger: { + id: "defaultBrowserCheck", + }, + template: "spotlight", + frequency: { + lifetime: 1, + }, + targeting: + "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && !(((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35) && os.isWindows && os.windowsVersion >= 6.1 && os.windowsBuildNumber < 22000", + }, + { + // Nimbus slug: device-migration-q4-spotlights-remaining-population-esr:treatment (message 3 of 3) + // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population-esr/summary#treatment + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + groups: ["eco"], + content: { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + modal: "tab", + screens: [ + { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_NEW_DEVICE", + content: { + logo: { + height: "149px", + imageURL: + "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/a43cd9cc-e8b2-477c-92f2-345557370de1.svg", + }, + title: { + fontSize: "24px", + string_id: + "device-migration-fxa-spotlight-getting-new-device-header-2", + letterSpacing: 0, + }, + subtitle: { + fontSize: "15px", + string_id: + "device-migration-fxa-spotlight-getting-new-device-body-2", + lineHeight: "1.4", + marginBlock: "8px 20px", + letterSpacing: 0, + paddingInline: "40px", + }, + dismiss_button: { + action: { + navigate: true, + }, + }, + primary_button: { + label: { + string_id: + "device-migration-fxa-spotlight-getting-new-device-primary-button", + paddingBlock: "4px", + paddingInline: "16px", + }, + action: { + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=new-device-in-your-future&entrypoint=device-migration-spotlight-experiment-v2", + where: "tabshifted", + }, + type: "OPEN_URL", + navigate: true, + }, + }, + }, + }, + ], + backdrop: "transparent", + template: "multistage", + transitions: true, + }, + trigger: { + id: "defaultBrowserCheck", + }, + template: "spotlight", + frequency: { + lifetime: 1, + }, + targeting: + "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && !(((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35) && !(os.isWindows && os.windowsVersion >= 6.1 && os.windowsBuildNumber < 22000)", + }, + { + // Nimbus slug: device-migration-q4-spotlights-remaining-population:treatment (message 1 of 3) + // Version range: 122+ + // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population/summary#treatment + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + groups: ["eco"], + content: { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + modal: "tab", + screens: [ + { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_BACKUP", + content: { + logo: { + height: "152px", + imageURL: + "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/c92a41e4-82cf-4ad5-8480-04a138bfb3cd.png", + }, + title: { + fontSize: "24px", + string_id: "device-migration-fxa-spotlight-heavy-user-header", + letterSpacing: 0, + }, + subtitle: { + fontSize: "15px", + string_id: "device-migration-fxa-spotlight-heavy-user-body", + lineHeight: "1.4", + marginBlock: "8px 20px", + letterSpacing: 0, + paddingInline: "20px", + }, + dismiss_button: { + action: { + navigate: true, + }, + }, + primary_button: { + label: { + string_id: + "device-migration-fxa-spotlight-heavy-user-primary-button", + paddingBlock: "4px", + paddingInline: "16px", + }, + action: { + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=dont-forget-to-backup&entrypoint=device-migration-spotlight-experiment-v2", + where: "tabshifted", + }, + type: "OPEN_URL", + navigate: true, + }, + }, + }, + }, + ], + backdrop: "transparent", + template: "multistage", + transitions: true, + }, + trigger: { + id: "defaultBrowserCheck", + }, + template: "spotlight", + frequency: { + lifetime: 1, + }, + targeting: + "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && (((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35)", + }, + { + // Nimbus slug: device-migration-q4-spotlights-remaining-population:treatment (message 2 of 3) + // Version range: 122+ + // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population/summary#treatment + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + groups: ["eco"], + content: { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + modal: "tab", + screens: [ + { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_PEACE", + content: { + logo: { + height: "133px", + imageURL: + "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/4a56d3ed-98c8-4a33-b853-b2cf7646efd8.png", + marginBlock: "22px -10px", + }, + title: { + fontSize: "24px", + string_id: + "device-migration-fxa-spotlight-older-device-header", + letterSpacing: 0, + }, + subtitle: { + fontSize: "15px", + string_id: "device-migration-fxa-spotlight-older-device-body", + lineHeight: "1.4", + marginBlock: "8px 20px", + letterSpacing: 0, + paddingInline: "40px", + }, + dismiss_button: { + action: { + navigate: true, + }, + }, + primary_button: { + label: { + string_id: + "device-migration-fxa-spotlight-older-device-primary-button", + marginBlock: "0 22px", + paddingBlock: "4px", + paddingInline: "16px", + }, + action: { + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=peace-of-mind&entrypoint=device-migration-spotlight-experiment-v2", + where: "tabshifted", + }, + type: "OPEN_URL", + navigate: true, + }, + }, + }, + }, + ], + backdrop: "transparent", + template: "multistage", + transitions: true, + }, + trigger: { + id: "defaultBrowserCheck", + }, + template: "spotlight", + frequency: { + lifetime: 1, + }, + targeting: + "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && !(((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35) && os.isWindows && os.windowsVersion >= 6.1 && os.windowsBuildNumber < 22000", + }, + { + // Nimbus slug: device-migration-q4-spotlights-remaining-population:treatment (message 3 of 3) + // Version range: 122+ + // Recipe: https://experimenter.services.mozilla.com/nimbus/device-migration-q4-spotlights-remaining-population/summary#treatment + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + groups: ["eco"], + content: { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT", + modal: "tab", + screens: [ + { + id: "Q4_DEVICE_MIGRATION_BACKUP_SPOTLIGHT_NEW_DEVICE", + content: { + logo: { + height: "149px", + imageURL: + "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/a43cd9cc-e8b2-477c-92f2-345557370de1.svg", + }, + title: { + fontSize: "24px", + string_id: + "device-migration-fxa-spotlight-getting-new-device-header-2", + letterSpacing: 0, + }, + subtitle: { + fontSize: "15px", + string_id: + "device-migration-fxa-spotlight-getting-new-device-body-2", + lineHeight: "1.4", + marginBlock: "8px 20px", + letterSpacing: 0, + paddingInline: "40px", + }, + dismiss_button: { + action: { + navigate: true, + }, + }, + primary_button: { + label: { + string_id: + "device-migration-fxa-spotlight-getting-new-device-primary-button", + paddingBlock: "4px", + paddingInline: "16px", + }, + action: { + data: { + args: "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/switching-devices?utm_source=spotlight-default&utm_medium=firefox-desktop&utm_campaign=migration&utm_content=new-device-in-your-future&entrypoint=device-migration-spotlight-experiment-v2", + where: "tabshifted", + }, + type: "OPEN_URL", + navigate: true, + }, + }, + }, + }, + ], + backdrop: "transparent", + template: "multistage", + transitions: true, + }, + trigger: { + id: "defaultBrowserCheck", + }, + template: "spotlight", + frequency: { + lifetime: 1, + }, + targeting: + "source == 'startup' && !willShowDefaultPrompt && 'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue && !usesFirefoxSync && !hasActiveEnterprisePolicies && userMonthlyActivity[userMonthlyActivity|length - 2][1]|date >= currentDate|date - (28 * 24 * 60 * 60 * 1000) && !(((currentDate|date - profileAgeCreated|date) / 86400000 >= 168) || totalBookmarksCount >= 35) && !(os.isWindows && os.windowsVersion >= 6.1 && os.windowsBuildNumber < 22000)", + }, + { // Nimbus slug: fox-doodle-set-to-default-early-day-user-de-fr-it-treatment-a-rollout:treatment-a // Version range: 116+ // Recipe: https://experimenter.services.mozilla.com/nimbus/fox-doodle-set-to-default-early-day-user-de-fr-it-treatment-a-rollout/summary#treatment-a diff --git a/browser/components/asrouter/tests/browser/browser.toml b/browser/components/asrouter/tests/browser/browser.toml index 7bed40373d..60ce42dfd8 100644 --- a/browser/components/asrouter/tests/browser/browser.toml +++ b/browser/components/asrouter/tests/browser/browser.toml @@ -21,6 +21,11 @@ skip-if = ["os == 'linux' && bits == 64 && !debug"] # Bug 1643036 ["browser_asrouter_infobar.js"] +["browser_asrouter_keyboard_cfr.js"] +https_first_disabled = true + +["browser_asrouter_milestone_message_cfr.js"] + ["browser_asrouter_momentspagehub.js"] tags = "remote-settings" diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js b/browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js index 19fcb63131..d22605d589 100644 --- a/browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js +++ b/browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js @@ -20,7 +20,7 @@ const { RemoteSettings } = ChromeUtils.importESModule( ); // This pref is used to override the Remote Settings server URL in tests. -// See SERVER_URL in services/settings/Utils.jsm for more details. +// See SERVER_URL in services/settings/Utils.sys.mjs for more details. const RS_SERVER_PREF = "services.settings.server"; const FLUENT_CONTENT = "asrouter-test-string = Test Test Test\n"; diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js b/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js index e29771c24f..1979c81a79 100644 --- a/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js +++ b/browser/components/asrouter/tests/browser/browser_asrouter_cfr.js @@ -115,14 +115,6 @@ function checkCFRAddonsElements(notification) { ); } -function checkCFRTrackingProtectionMilestone(notification) { - Assert.ok(notification.hidden === false, "Panel should be visible"); - Assert.ok( - notification.getAttribute("data-notification-category") === "short_message", - "Panel have correct data attribute" - ); -} - function clearNotifications() { for (let notification of PopupNotifications._currentNotifications) { notification.remove(); @@ -498,59 +490,6 @@ add_task(async function test_cfr_addon_install() { Services.fog.testResetFOG(); }); -add_task( - async function test_cfr_tracking_protection_milestone_notification_remove() { - await SpecialPowers.pushPrefEnv({ - set: [ - ["browser.contentblocking.cfr-milestone.milestone-achieved", 1000], - [ - "browser.newtabpage.activity-stream.asrouter.providers.cfr", - `{"id":"cfr","enabled":true,"type":"local","localProvider":"CFRMessageProvider","updateCycleInMs":3600000}`, - ], - ], - }); - - // addRecommendation checks that scheme starts with http and host matches - let browser = gBrowser.selectedBrowser; - BrowserTestUtils.startLoadingURIString(browser, "http://example.com/"); - await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/"); - - const showPanel = BrowserTestUtils.waitForEvent( - PopupNotifications.panel, - "popupshown" - ); - - Services.obs.notifyObservers( - { - wrappedJSObject: { - event: "ContentBlockingMilestone", - }, - }, - "SiteProtection:ContentBlockingMilestone" - ); - - await showPanel; - - const notification = document.getElementById( - "contextual-feature-recommendation-notification" - ); - - checkCFRTrackingProtectionMilestone(notification); - - Assert.ok(notification.secondaryButton); - let hidePanel = BrowserTestUtils.waitForEvent( - PopupNotifications.panel, - "popuphidden" - ); - - notification.secondaryButton.click(); - await hidePanel; - await SpecialPowers.popPrefEnv(); - clearNotifications(); - Services.fog.testResetFOG(); - } -); - add_task(async function test_cfr_addon_and_features_show() { // addRecommendation checks that scheme starts with http and host matches let browser = gBrowser.selectedBrowser; @@ -747,62 +686,6 @@ add_task(async function test_providerNames() { } }); -add_task(async function test_cfr_notification_keyboard() { - // addRecommendation checks that scheme starts with http and host matches - const browser = gBrowser.selectedBrowser; - BrowserTestUtils.startLoadingURIString(browser, "http://example.com/"); - await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/"); - - const response = await trigger_cfr_panel(browser, "example.com"); - Assert.ok( - response, - "Should return true if addRecommendation checks were successful" - ); - - // Open the panel with the keyboard. - // Toolbar buttons aren't always focusable; toolbar keyboard navigation - // makes them focusable on demand. Therefore, we must force focus. - const button = document.getElementById("contextual-feature-recommendation"); - button.setAttribute("tabindex", "-1"); - button.focus(); - button.removeAttribute("tabindex"); - - let focused = BrowserTestUtils.waitForEvent( - PopupNotifications.panel, - "focus", - true - ); - EventUtils.synthesizeKey(" "); - await focused; - Assert.ok(true, "Focus inside panel after button pressed"); - - let hidden = BrowserTestUtils.waitForEvent( - PopupNotifications.panel, - "popuphidden" - ); - EventUtils.synthesizeKey("KEY_Escape"); - await hidden; - Assert.ok(true, "Panel hidden after Escape pressed"); - - const showPanel = BrowserTestUtils.waitForEvent( - PopupNotifications.panel, - "popupshown" - ); - // Need to dismiss the notification to clear the RecommendationMap - document.getElementById("contextual-feature-recommendation").click(); - await showPanel; - - const hidePanel = BrowserTestUtils.waitForEvent( - PopupNotifications.panel, - "popuphidden" - ); - document - .getElementById("contextual-feature-recommendation-notification") - .button.click(); - await hidePanel; - Services.fog.testResetFOG(); -}); - add_task(function test_updateCycleForProviders() { Services.prefs .getChildList("browser.newtabpage.activity-stream.asrouter.providers.") diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_keyboard_cfr.js b/browser/components/asrouter/tests/browser/browser_asrouter_keyboard_cfr.js new file mode 100644 index 0000000000..c3dfc0b0bb --- /dev/null +++ b/browser/components/asrouter/tests/browser/browser_asrouter_keyboard_cfr.js @@ -0,0 +1,162 @@ +/* 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/. */ + +/* eslint-disable @microsoft/sdl/no-insecure-url */ +function clearNotifications() { + for (let notification of PopupNotifications._currentNotifications) { + notification.remove(); + } + + // Clicking the primary action also removes the notification + Assert.equal( + PopupNotifications._currentNotifications.length, + 0, + "Should have removed the notification" + ); +} + +add_setup(async function () { + // Store it in order to restore to the original value + const { _fetchLatestAddonVersion } = CFRPageActions; + // Prevent fetching the real addon url and making a network request + CFRPageActions._fetchLatestAddonVersion = () => "http://example.com"; + Services.fog.testResetFOG(); + + registerCleanupFunction(() => { + CFRPageActions._fetchLatestAddonVersion = _fetchLatestAddonVersion; + clearNotifications(); + CFRPageActions.clearRecommendations(); + }); +}); + +add_task(async function test_cfr_notification_keyboard() { + // addRecommendation checks that scheme starts with http and host matches + const browser = gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(browser, "http://example.com/"); + await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/"); + + clearNotifications(); + + let recommendation = { + template: "cfr_doorhanger", + groups: ["mochitest-group"], + content: { + layout: "addon_recommendation", + category: "cfrAddons", + anchor_id: "page-action-buttons", + icon_class: "cfr-doorhanger-medium-icon", + skip_address_bar_notifier: false, + heading_text: "Sample Mochitest", + icon: "chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg", + icon_dark_theme: + "chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg", + info_icon: { + label: { attributes: { tooltiptext: "Why am I seeing this" } }, + sumo_path: "extensionrecommendations", + }, + addon: { + id: "addon-id", + title: "Addon name", + icon: "chrome://browser/skin/addons/addon-install-downloading.svg", + author: "Author name", + amo_url: "https://example.com", + rating: "4.5", + users: "1.1M", + }, + text: "Mochitest", + buttons: { + primary: { + label: { + value: "OK", + attributes: { accesskey: "O" }, + }, + action: { + type: "CANCEL", + data: {}, + }, + }, + secondary: [ + { + label: { + value: "Cancel", + attributes: { accesskey: "C" }, + }, + action: { + type: "CANCEL", + }, + }, + ], + }, + }, + }; + + recommendation.content.notification_text = new String("Mochitest"); // eslint-disable-line + recommendation.content.notification_text.attributes = { + tooltiptext: "Mochitest tooltip", + "a11y-announcement": "Mochitest announcement", + }; + + const response = await CFRPageActions.addRecommendation( + gBrowser.selectedBrowser, + "example.com", + recommendation, + // Use the real AS dispatch method to trigger real notifications + ASRouter.dispatchCFRAction + ); + + Assert.ok( + response, + "Should return true if addRecommendation checks were successful" + ); + + // Open the panel with the keyboard. + // Toolbar buttons aren't always focusable; toolbar keyboard navigation + // makes them focusable on demand. Therefore, we must force focus. + const button = document.getElementById("contextual-feature-recommendation"); + button.setAttribute("tabindex", "-1"); + + let buttonFocused = BrowserTestUtils.waitForEvent(button, "focus"); + button.focus(); + await buttonFocused; + + Assert.ok(true, "Focus page action button"); + + let focused = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "focus", + true + ); + + EventUtils.synthesizeKey(" "); + await focused; + Assert.ok(true, "Focus inside panel after button pressed"); + + button.removeAttribute("tabindex"); + + let hidden = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + EventUtils.synthesizeKey("KEY_Escape"); + await hidden; + Assert.ok(true, "Panel hidden after Escape pressed"); + + const showPanel = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + // Need to dismiss the notification to clear the RecommendationMap + document.getElementById("contextual-feature-recommendation").click(); + await showPanel; + + const hidePanel = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + document + .getElementById("contextual-feature-recommendation-notification") + .button.click(); + await hidePanel; + Services.fog.testResetFOG(); +}); diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_milestone_message_cfr.js b/browser/components/asrouter/tests/browser/browser_asrouter_milestone_message_cfr.js new file mode 100644 index 0000000000..6585963d6f --- /dev/null +++ b/browser/components/asrouter/tests/browser/browser_asrouter_milestone_message_cfr.js @@ -0,0 +1,78 @@ +/* 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/. */ + +/* eslint-disable @microsoft/sdl/no-insecure-url */ +function checkCFRTrackingProtectionMilestone(notification) { + Assert.ok(notification.hidden === false, "Panel should be visible"); + Assert.ok( + notification.getAttribute("data-notification-category") === "short_message", + "Panel have correct data attribute" + ); +} + +function clearNotifications() { + for (let notification of PopupNotifications._currentNotifications) { + notification.remove(); + } + + // Clicking the primary action also removes the notification + Assert.equal( + PopupNotifications._currentNotifications.length, + 0, + "Should have removed the notification" + ); +} + +add_task( + async function test_cfr_tracking_protection_milestone_notification_remove() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.contentblocking.cfr-milestone.milestone-achieved", 1000], + [ + "browser.newtabpage.activity-stream.asrouter.providers.cfr", + `{"id":"cfr","enabled":true,"type":"local","localProvider":"CFRMessageProvider","updateCycleInMs":3600000}`, + ], + ], + }); + + // addRecommendation checks that scheme starts with http and host matches + let browser = gBrowser.selectedBrowser; + BrowserTestUtils.startLoadingURIString(browser, "http://example.com/"); + await BrowserTestUtils.browserLoaded(browser, false, "http://example.com/"); + + const showPanel = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popupshown" + ); + + Services.obs.notifyObservers( + { + wrappedJSObject: { + event: "ContentBlockingMilestone", + }, + }, + "SiteProtection:ContentBlockingMilestone" + ); + + await showPanel; + + const notification = document.getElementById( + "contextual-feature-recommendation-notification" + ); + + checkCFRTrackingProtectionMilestone(notification); + + Assert.ok(notification.secondaryButton); + let hidePanel = BrowserTestUtils.waitForEvent( + PopupNotifications.panel, + "popuphidden" + ); + + notification.secondaryButton.click(); + await hidePanel; + await SpecialPowers.popPrefEnv(); + clearNotifications(); + Services.fog.testResetFOG(); + } +); diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js b/browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js index f752d01116..aea702bb61 100644 --- a/browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js +++ b/browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js @@ -14,11 +14,11 @@ const { ASRouter } = ChromeUtils.importESModule( const HOMEPAGE_OVERRIDE_PREF = "browser.startup.homepage_override.once"; add_task(async function test_with_rs_messages() { - // Force the WNPanel provider cache to 0 by modifying updateCycleInMs + // Force the cfr provider cache to 0 by modifying updateCycleInMs await SpecialPowers.pushPrefEnv({ set: [ [ - "browser.newtabpage.activity-stream.asrouter.providers.whats-new-panel", + "browser.newtabpage.activity-stream.asrouter.providers.cfr", `{"id":"cfr","enabled":true,"type":"remote-settings","collection":"cfr","updateCycleInMs":0}`, ], ], diff --git a/browser/components/asrouter/tests/unit/ASRouter.test.js b/browser/components/asrouter/tests/unit/ASRouter.test.js index 7df1449a14..1f5899fce1 100644 --- a/browser/components/asrouter/tests/unit/ASRouter.test.js +++ b/browser/components/asrouter/tests/unit/ASRouter.test.js @@ -48,7 +48,6 @@ describe("ASRouter", () => { let fakeAttributionCode; let fakeTargetingContext; let FakeToolbarBadgeHub; - let FakeToolbarPanelHub; let FakeMomentsPageHub; let ASRouterTargeting; let screenImpressions; @@ -151,7 +150,6 @@ describe("ASRouter", () => { cfr: "", "message-groups": "", "messaging-experiments": "", - "whats-new-panel": "", }, totalBookmarksCount: {}, firefoxVersion: 80, @@ -159,7 +157,6 @@ describe("ASRouter", () => { needsUpdate: {}, hasPinnedTabs: false, hasAccessedFxAPanel: false, - isWhatsNewPanelEnabled: true, userPrefs: { cfrFeatures: true, cfrAddons: true, @@ -203,12 +200,6 @@ describe("ASRouter", () => { writeAttributionFile: () => Promise.resolve(), getCachedAttributionData: sinon.stub(), }; - FakeToolbarPanelHub = { - init: sandbox.stub(), - uninit: sandbox.stub(), - forceShowMessage: sandbox.stub(), - enableToolbarButton: sandbox.stub(), - }; FakeToolbarBadgeHub = { init: sandbox.stub(), uninit: sandbox.stub(), @@ -252,7 +243,6 @@ describe("ASRouter", () => { PanelTestProvider, MacAttribution: { applicationPath: "" }, ToolbarBadgeHub: FakeToolbarBadgeHub, - ToolbarPanelHub: FakeToolbarPanelHub, MomentsPageHub: FakeMomentsPageHub, KintoHttpClient: class { bucket() { @@ -354,7 +344,6 @@ describe("ASRouter", () => { // ASRouter init called in `beforeEach` block above assert.calledOnce(FakeToolbarBadgeHub.init); - assert.calledOnce(FakeToolbarPanelHub.init); assert.calledOnce(FakeMomentsPageHub.init); assert.calledWithExactly( @@ -370,15 +359,6 @@ describe("ASRouter", () => { ); assert.calledWithExactly( - FakeToolbarPanelHub.init, - Router.waitForInitialized, - { - getMessages: Router.handleMessageRequest, - sendTelemetry: Router.sendTelemetry, - } - ); - - assert.calledWithExactly( FakeMomentsPageHub.init, Router.waitForInitialized, { @@ -678,25 +658,10 @@ describe("ASRouter", () => { sandbox.stub(CFRPageActions, "addRecommendation"); browser = {}; }); - it("should route whatsnew_panel_message message to the right hub", () => { - Router.routeCFRMessage( - { template: "whatsnew_panel_message" }, - browser, - "", - true - ); - - assert.calledOnce(FakeToolbarPanelHub.forceShowMessage); - assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener); - assert.notCalled(CFRPageActions.addRecommendation); - assert.notCalled(CFRPageActions.forceRecommendation); - assert.notCalled(FakeMomentsPageHub.executeAction); - }); it("should route moments messages to the right hub", () => { Router.routeCFRMessage({ template: "update_action" }, browser, "", true); assert.calledOnce(FakeMomentsPageHub.executeAction); - assert.notCalled(FakeToolbarPanelHub.forceShowMessage); assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener); assert.notCalled(CFRPageActions.addRecommendation); assert.notCalled(CFRPageActions.forceRecommendation); @@ -705,7 +670,6 @@ describe("ASRouter", () => { Router.routeCFRMessage({ template: "toolbar_badge" }, browser); assert.calledOnce(FakeToolbarBadgeHub.registerBadgeNotificationListener); - assert.notCalled(FakeToolbarPanelHub.forceShowMessage); assert.notCalled(CFRPageActions.addRecommendation); assert.notCalled(CFRPageActions.forceRecommendation); assert.notCalled(FakeMomentsPageHub.executeAction); @@ -721,7 +685,6 @@ describe("ASRouter", () => { assert.calledOnce(CFRPageActions.addRecommendation); assert.notCalled(CFRPageActions.forceRecommendation); assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener); - assert.notCalled(FakeToolbarPanelHub.forceShowMessage); assert.notCalled(FakeMomentsPageHub.executeAction); }); it("should route cfr_doorhanger message to the right hub force = false", () => { @@ -733,7 +696,6 @@ describe("ASRouter", () => { ); assert.calledOnce(CFRPageActions.addRecommendation); - assert.notCalled(FakeToolbarPanelHub.forceShowMessage); assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener); assert.notCalled(CFRPageActions.forceRecommendation); assert.notCalled(FakeMomentsPageHub.executeAction); @@ -742,7 +704,6 @@ describe("ASRouter", () => { Router.routeCFRMessage({ template: "cfr_doorhanger" }, browser, {}, true); assert.calledOnce(CFRPageActions.forceRecommendation); - assert.notCalled(FakeToolbarPanelHub.forceShowMessage); assert.notCalled(CFRPageActions.addRecommendation); assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener); assert.notCalled(FakeMomentsPageHub.executeAction); @@ -759,7 +720,6 @@ describe("ASRouter", () => { const { args } = CFRPageActions.addRecommendation.firstCall; // Host should be null assert.isNull(args[1]); - assert.notCalled(FakeToolbarPanelHub.forceShowMessage); assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener); assert.notCalled(CFRPageActions.forceRecommendation); assert.notCalled(FakeMomentsPageHub.executeAction); @@ -773,7 +733,6 @@ describe("ASRouter", () => { ); assert.calledOnce(CFRPageActions.forceRecommendation); - assert.notCalled(FakeToolbarPanelHub.forceShowMessage); assert.notCalled(CFRPageActions.addRecommendation); assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener); assert.notCalled(FakeMomentsPageHub.executeAction); @@ -786,7 +745,6 @@ describe("ASRouter", () => { true ); - assert.notCalled(FakeToolbarPanelHub.forceShowMessage); assert.notCalled(CFRPageActions.forceRecommendation); assert.notCalled(CFRPageActions.addRecommendation); assert.notCalled(FakeToolbarBadgeHub.registerBadgeNotificationListener); @@ -961,7 +919,6 @@ describe("ASRouter", () => { type: "local", enabled: true, messages: [ - "whatsnew_panel_message", "cfr_doorhanger", "toolbar_badge", "update_action", @@ -1272,43 +1229,6 @@ describe("ASRouter", () => { Router.state.messageImpressions ); }); - it("should return all unblocked messages that match the template, trigger if returnAll=true", async () => { - const message1 = { - provider: "whats_new", - id: "1", - template: "whatsnew_panel_message", - trigger: { id: "whatsNewPanelOpened" }, - groups: ["whats_new"], - }; - const message2 = { - provider: "whats_new", - id: "2", - template: "whatsnew_panel_message", - trigger: { id: "whatsNewPanelOpened" }, - groups: ["whats_new"], - }; - const message3 = { - provider: "whats_new", - id: "3", - template: "badge", - groups: ["whats_new"], - }; - ASRouterTargeting.findMatchingMessage.callsFake(() => [ - message2, - message1, - ]); - await Router.setState({ - messages: [message3, message2, message1], - providers: [{ id: "whats_new" }], - }); - const result = await Router.handleMessageRequest({ - template: "whatsnew_panel_message", - triggerId: "whatsNewPanelOpened", - returnAll: true, - }); - - assert.deepEqual(result, [message2, message1]); - }); it("should forward trigger param info", async () => { const trigger = { triggerId: "foo", @@ -1854,33 +1774,6 @@ describe("ASRouter", () => { }); }); - describe("#forceWNPanel", () => { - let browser = { - ownerGlobal: { - document: new Document(), - PanelUI: { - showSubView: sinon.stub(), - panel: { - setAttribute: sinon.stub(), - }, - }, - }, - }; - let fakePanel = { - setAttribute: sinon.stub(), - }; - sinon - .stub(browser.ownerGlobal.document, "getElementById") - .returns(fakePanel); - - it("should call enableToolbarButton", async () => { - await Router.forceWNPanel(browser); - assert.calledOnce(FakeToolbarPanelHub.enableToolbarButton); - assert.calledOnce(browser.ownerGlobal.PanelUI.showSubView); - assert.calledWith(fakePanel.setAttribute, "noautohide", true); - }); - }); - describe("_triggerHandler", () => { it("should call #sendTriggerMessage with the correct trigger", () => { const getter = sandbox.stub(); diff --git a/browser/components/asrouter/tests/unit/ASRouterChild.test.js b/browser/components/asrouter/tests/unit/ASRouterChild.test.js index b73e56d510..c6533e073d 100644 --- a/browser/components/asrouter/tests/unit/ASRouterChild.test.js +++ b/browser/components/asrouter/tests/unit/ASRouterChild.test.js @@ -1,6 +1,6 @@ /*eslint max-nested-callbacks: ["error", 10]*/ import { ASRouterChild } from "actors/ASRouterChild.sys.mjs"; -import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.sys.mjs"; +import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.mjs"; describe("ASRouterChild", () => { let asRouterChild = null; @@ -24,7 +24,6 @@ describe("ASRouterChild", () => { msg.DISABLE_PROVIDER, msg.ENABLE_PROVIDER, msg.EXPIRE_QUERY_CACHE, - msg.FORCE_WHATSNEW_PANEL, msg.IMPRESSION, msg.RESET_PROVIDER_PREF, msg.SET_PROVIDER_USER_PREF, diff --git a/browser/components/asrouter/tests/unit/ASRouterParent.test.js b/browser/components/asrouter/tests/unit/ASRouterParent.test.js index 0358b1261c..e65d7db825 100644 --- a/browser/components/asrouter/tests/unit/ASRouterParent.test.js +++ b/browser/components/asrouter/tests/unit/ASRouterParent.test.js @@ -1,5 +1,5 @@ import { ASRouterParent } from "actors/ASRouterParent.sys.mjs"; -import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.sys.mjs"; +import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.mjs"; describe("ASRouterParent", () => { let asRouterParent = null; diff --git a/browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js b/browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js index 7bfec3e099..6a965c5689 100644 --- a/browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js +++ b/browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js @@ -1,6 +1,6 @@ import { ASRouterParentProcessMessageHandler } from "modules/ASRouterParentProcessMessageHandler.sys.mjs"; import { _ASRouter } from "modules/ASRouter.sys.mjs"; -import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.sys.mjs"; +import { MESSAGE_TYPE_HASH as msg } from "modules/ActorConstants.mjs"; describe("ASRouterParentProcessMessageHandler", () => { let handler = null; @@ -14,8 +14,6 @@ describe("ASRouterParentProcessMessageHandler", () => { "addImpression", "evaluateExpression", "forceAttribution", - "forceWNPanel", - "closeWNPanel", "forcePBWindow", "resetGroupsState", "resetMessageState", @@ -122,7 +120,6 @@ describe("ASRouterParentProcessMessageHandler", () => { [ msg.AS_ROUTER_TELEMETRY_USER_EVENT, msg.TOOLBAR_BADGE_TELEMETRY, - msg.TOOLBAR_PANEL_TELEMETRY, msg.MOMENTS_PAGE_TELEMETRY, msg.DOORHANGER_TELEMETRY, ].forEach(type => { @@ -309,28 +306,6 @@ describe("ASRouterParentProcessMessageHandler", () => { assert.calledOnce(config.router.forceAttribution); }); }); - describe("FORCE_WHATSNEW_PANEL action", () => { - it("default calls forceWNPanel", () => { - handler.handleMessage( - msg.FORCE_WHATSNEW_PANEL, - {}, - { browser: { ownerGlobal: {} } } - ); - assert.calledOnce(config.router.forceWNPanel); - assert.calledWith(config.router.forceWNPanel, { ownerGlobal: {} }); - }); - }); - describe("CLOSE_WHATSNEW_PANEL action", () => { - it("default calls closeWNPanel", () => { - handler.handleMessage( - msg.CLOSE_WHATSNEW_PANEL, - {}, - { browser: { ownerGlobal: {} } } - ); - assert.calledOnce(config.router.closeWNPanel); - assert.calledWith(config.router.closeWNPanel, { ownerGlobal: {} }); - }); - }); describe("FORCE_PRIVATE_BROWSING_WINDOW action", () => { it("default calls forcePBWindow", () => { handler.handleMessage( diff --git a/browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js b/browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js index 3e91b657bc..cfeac77025 100644 --- a/browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js +++ b/browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js @@ -1,10 +1,6 @@ import { _ToolbarBadgeHub } from "modules/ToolbarBadgeHub.sys.mjs"; import { GlobalOverrider } from "test/unit/utils"; import { OnboardingMessageProvider } from "modules/OnboardingMessageProvider.sys.mjs"; -import { - _ToolbarPanelHub, - ToolbarPanelHub, -} from "modules/ToolbarPanelHub.sys.mjs"; describe("ToolbarBadgeHub", () => { let sandbox; @@ -13,7 +9,6 @@ describe("ToolbarBadgeHub", () => { let fakeSendTelemetry; let isBrowserPrivateStub; let fxaMessage; - let whatsnewMessage; let fakeElement; let globals; let everyWindowStub; @@ -36,28 +31,6 @@ describe("ToolbarBadgeHub", () => { const onboardingMsgs = await OnboardingMessageProvider.getUntranslatedMessages(); fxaMessage = onboardingMsgs.find(({ id }) => id === "FXA_ACCOUNTS_BADGE"); - whatsnewMessage = { - id: `WHATS_NEW_BADGE_71`, - template: "toolbar_badge", - content: { - delay: 1000, - target: "whats-new-menu-button", - action: { id: "show-whatsnew-button" }, - badgeDescription: { string_id: "cfr-badge-reader-label-newfeature" }, - }, - priority: 1, - trigger: { id: "toolbarBadgeUpdate" }, - frequency: { - // Makes it so that we track impressions for this message while at the - // same time it can have unlimited impressions - lifetime: Infinity, - }, - // Never saw this message or saw it in the past 4 days or more recent - targeting: `isWhatsNewPanelEnabled && - (!messageImpressions['WHATS_NEW_BADGE_71'] || - (messageImpressions['WHATS_NEW_BADGE_71']|length >= 1 && - currentDate|date - messageImpressions['WHATS_NEW_BADGE_71'][0] <= 4 * 24 * 3600 * 1000))`, - }; fakeElement = { classList: { add: sandbox.stub(), @@ -93,7 +66,6 @@ describe("ToolbarBadgeHub", () => { setStringPrefStub = sandbox.stub(); requestIdleCallbackStub = sandbox.stub().callsFake(fn => fn()); globals.set({ - ToolbarPanelHub, requestIdleCallback: requestIdleCallbackStub, EveryWindow: everyWindowStub, PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub }, @@ -139,16 +111,6 @@ describe("ToolbarBadgeHub", () => { assert.calledTwice(instance.messageRequest); }); - it("should add a pref observer", async () => { - await instance.init(sandbox.stub().resolves(), {}); - - assert.calledOnce(addObserverStub); - assert.calledWithExactly( - addObserverStub, - instance.prefs.WHATSNEW_TOOLBAR_PANEL, - instance - ); - }); }); describe("#uninit", () => { beforeEach(async () => { @@ -164,16 +126,6 @@ describe("ToolbarBadgeHub", () => { assert.calledOnce(clearTimeoutStub); assert.calledWithExactly(clearTimeoutStub, 2); }); - it("should remove the pref observer", () => { - instance.uninit(); - - assert.calledOnce(removeObserverStub); - assert.calledWithExactly( - removeObserverStub, - instance.prefs.WHATSNEW_TOOLBAR_PANEL, - instance - ); - }); }); describe("messageRequest", () => { let handleMessageRequestStub; @@ -293,66 +245,6 @@ describe("ToolbarBadgeHub", () => { instance.removeAllNotifications ); }); - it("should execute actions if they exist", () => { - sandbox.stub(instance, "executeAction"); - instance.addToolbarNotification(target, whatsnewMessage); - - assert.calledOnce(instance.executeAction); - assert.calledWithExactly(instance.executeAction, { - ...whatsnewMessage.content.action, - message_id: whatsnewMessage.id, - }); - }); - it("should create a description element", () => { - sandbox.stub(instance, "executeAction"); - instance.addToolbarNotification(target, whatsnewMessage); - - assert.calledOnce(fakeDocument.createElement); - assert.calledWithExactly(fakeDocument.createElement, "span"); - }); - it("should set description id to element and to button", () => { - sandbox.stub(instance, "executeAction"); - instance.addToolbarNotification(target, whatsnewMessage); - - assert.calledWithExactly( - fakeElement.setAttribute, - "id", - "toolbarbutton-notification-description" - ); - assert.calledWithExactly( - fakeElement.setAttribute, - "aria-labelledby", - `toolbarbutton-notification-description ${whatsnewMessage.content.target}` - ); - }); - it("should attach fluent id to description", () => { - sandbox.stub(instance, "executeAction"); - instance.addToolbarNotification(target, whatsnewMessage); - - assert.calledOnce(fakeDocument.l10n.setAttributes); - assert.calledWithExactly( - fakeDocument.l10n.setAttributes, - fakeElement, - whatsnewMessage.content.badgeDescription.string_id - ); - }); - it("should add an impression for the message", () => { - instance.addToolbarNotification(target, whatsnewMessage); - - assert.calledOnce(instance._addImpression); - assert.calledWithExactly(instance._addImpression, whatsnewMessage); - }); - it("should send an impression ping", async () => { - sandbox.stub(instance, "sendUserEventTelemetry"); - instance.addToolbarNotification(target, whatsnewMessage); - - assert.calledOnce(instance.sendUserEventTelemetry); - assert.calledWithExactly( - instance.sendUserEventTelemetry, - "IMPRESSION", - whatsnewMessage - ); - }); }); describe("registerBadgeNotificationListener", () => { let msg_no_delay; @@ -410,44 +302,6 @@ describe("ToolbarBadgeHub", () => { assert.calledOnce(everyWindowStub.unregisterCallback); assert.calledWithExactly(everyWindowStub.unregisterCallback, instance.id); }); - it("should only call executeAction for 'update_action' messages", () => { - const stub = sandbox.stub(instance, "executeAction"); - const updateActionMsg = { ...msg_no_delay, template: "update_action" }; - - instance.registerBadgeNotificationListener(updateActionMsg); - - assert.notCalled(everyWindowStub.registerCallback); - assert.calledOnce(stub); - }); - }); - describe("executeAction", () => { - let blockMessageByIdStub; - beforeEach(async () => { - blockMessageByIdStub = sandbox.stub(); - await instance.init(sandbox.stub().resolves(), { - blockMessageById: blockMessageByIdStub, - }); - }); - it("should call ToolbarPanelHub.enableToolbarButton", () => { - const stub = sandbox.stub( - _ToolbarPanelHub.prototype, - "enableToolbarButton" - ); - - instance.executeAction({ id: "show-whatsnew-button" }); - - assert.calledOnce(stub); - }); - it("should call ToolbarPanelHub.enableAppmenuButton", () => { - const stub = sandbox.stub( - _ToolbarPanelHub.prototype, - "enableAppmenuButton" - ); - - instance.executeAction({ id: "show-whatsnew-button" }); - - assert.calledOnce(stub); - }); }); describe("removeToolbarNotification", () => { it("should remove the notification", () => { @@ -629,24 +483,4 @@ describe("ToolbarBadgeHub", () => { assert.propertyVal(ping.data, "event", "CLICK"); }); }); - describe("#observe", () => { - it("should make a message request when the whats new pref is changed", () => { - sandbox.stub(instance, "messageRequest"); - - instance.observe("", "", instance.prefs.WHATSNEW_TOOLBAR_PANEL); - - assert.calledOnce(instance.messageRequest); - assert.calledWithExactly(instance.messageRequest, { - template: "toolbar_badge", - triggerId: "toolbarBadgeUpdate", - }); - }); - it("should not react to other pref changes", () => { - sandbox.stub(instance, "messageRequest"); - - instance.observe("", "", "foo"); - - assert.notCalled(instance.messageRequest); - }); - }); }); diff --git a/browser/components/asrouter/tests/unit/ToolbarPanelHub.test.js b/browser/components/asrouter/tests/unit/ToolbarPanelHub.test.js deleted file mode 100644 index 1755f62308..0000000000 --- a/browser/components/asrouter/tests/unit/ToolbarPanelHub.test.js +++ /dev/null @@ -1,760 +0,0 @@ -import { _ToolbarPanelHub } from "modules/ToolbarPanelHub.sys.mjs"; -import { GlobalOverrider } from "test/unit/utils"; -import { PanelTestProvider } from "modules/PanelTestProvider.sys.mjs"; - -describe("ToolbarPanelHub", () => { - let globals; - let sandbox; - let instance; - let everyWindowStub; - let fakeDocument; - let fakeWindow; - let fakeElementById; - let fakeElementByTagName; - let createdCustomElements = []; - let eventListeners = {}; - let addObserverStub; - let removeObserverStub; - let getBoolPrefStub; - let setBoolPrefStub; - let waitForInitializedStub; - let isBrowserPrivateStub; - let fakeSendTelemetry; - let getEarliestRecordedDateStub; - let getEventsByDateRangeStub; - let defaultSearchStub; - let scriptloaderStub; - let fakeRemoteL10n; - let getViewNodeStub; - - beforeEach(async () => { - sandbox = sinon.createSandbox(); - globals = new GlobalOverrider(); - instance = new _ToolbarPanelHub(); - waitForInitializedStub = sandbox.stub().resolves(); - fakeElementById = { - setAttribute: sandbox.stub(), - removeAttribute: sandbox.stub(), - querySelector: sandbox.stub().returns(null), - querySelectorAll: sandbox.stub().returns([]), - appendChild: sandbox.stub(), - addEventListener: sandbox.stub(), - hasAttribute: sandbox.stub(), - toggleAttribute: sandbox.stub(), - remove: sandbox.stub(), - removeChild: sandbox.stub(), - }; - fakeElementByTagName = { - setAttribute: sandbox.stub(), - removeAttribute: sandbox.stub(), - querySelector: sandbox.stub().returns(null), - querySelectorAll: sandbox.stub().returns([]), - appendChild: sandbox.stub(), - addEventListener: sandbox.stub(), - hasAttribute: sandbox.stub(), - toggleAttribute: sandbox.stub(), - remove: sandbox.stub(), - removeChild: sandbox.stub(), - }; - fakeDocument = { - getElementById: sandbox.stub().returns(fakeElementById), - getElementsByTagName: sandbox.stub().returns(fakeElementByTagName), - querySelector: sandbox.stub().returns({}), - createElement: tagName => { - const element = { - tagName, - classList: {}, - addEventListener: (ev, fn) => { - eventListeners[ev] = fn; - }, - appendChild: sandbox.stub(), - setAttribute: sandbox.stub(), - textContent: "", - }; - element.classList.add = sandbox.stub(); - element.classList.includes = className => - element.classList.add.firstCall.args[0] === className; - createdCustomElements.push(element); - return element; - }, - l10n: { - translateElements: sandbox.stub(), - translateFragment: sandbox.stub(), - formatMessages: sandbox.stub().resolves([{}]), - setAttributes: sandbox.stub(), - }, - }; - fakeWindow = { - // eslint-disable-next-line object-shorthand - DocumentFragment: function () { - return fakeElementById; - }, - document: fakeDocument, - browser: { - ownerDocument: fakeDocument, - }, - MozXULElement: { insertFTLIfNeeded: sandbox.stub() }, - ownerGlobal: { - openLinkIn: sandbox.stub(), - gBrowser: "gBrowser", - }, - PanelUI: { - panel: fakeElementById, - whatsNewPanel: fakeElementById, - }, - customElements: { get: sandbox.stub() }, - }; - everyWindowStub = { - registerCallback: sandbox.stub(), - unregisterCallback: sandbox.stub(), - }; - scriptloaderStub = { loadSubScript: sandbox.stub() }; - addObserverStub = sandbox.stub(); - removeObserverStub = sandbox.stub(); - getBoolPrefStub = sandbox.stub(); - setBoolPrefStub = sandbox.stub(); - fakeSendTelemetry = sandbox.stub(); - isBrowserPrivateStub = sandbox.stub(); - getEarliestRecordedDateStub = sandbox.stub().returns( - // A random date that's not the current timestamp - new Date() - 500 - ); - getEventsByDateRangeStub = sandbox.stub().returns([]); - getViewNodeStub = sandbox.stub().returns(fakeElementById); - defaultSearchStub = { defaultEngine: { name: "DDG" } }; - fakeRemoteL10n = { - l10n: {}, - reloadL10n: sandbox.stub(), - createElement: sandbox - .stub() - .callsFake((doc, el) => fakeDocument.createElement(el)), - }; - globals.set({ - EveryWindow: everyWindowStub, - Services: { - ...Services, - prefs: { - addObserver: addObserverStub, - removeObserver: removeObserverStub, - getBoolPref: getBoolPrefStub, - setBoolPref: setBoolPrefStub, - }, - search: defaultSearchStub, - scriptloader: scriptloaderStub, - }, - PrivateBrowsingUtils: { - isBrowserPrivate: isBrowserPrivateStub, - }, - TrackingDBService: { - getEarliestRecordedDate: getEarliestRecordedDateStub, - getEventsByDateRange: getEventsByDateRangeStub, - }, - SpecialMessageActions: { - handleAction: sandbox.stub(), - }, - RemoteL10n: fakeRemoteL10n, - PanelMultiView: { - getViewNode: getViewNodeStub, - }, - }); - }); - afterEach(() => { - instance.uninit(); - sandbox.restore(); - globals.restore(); - eventListeners = {}; - createdCustomElements = []; - }); - it("should create an instance", () => { - assert.ok(instance); - }); - it("should enableAppmenuButton() on init() just once", async () => { - instance.enableAppmenuButton = sandbox.stub(); - - await instance.init(waitForInitializedStub, { getMessages: () => {} }); - await instance.init(waitForInitializedStub, { getMessages: () => {} }); - - assert.calledOnce(instance.enableAppmenuButton); - - instance.uninit(); - - await instance.init(waitForInitializedStub, { getMessages: () => {} }); - - assert.calledTwice(instance.enableAppmenuButton); - }); - it("should unregisterCallback on uninit()", () => { - instance.uninit(); - assert.calledTwice(everyWindowStub.unregisterCallback); - }); - describe("#maybeLoadCustomElement", () => { - it("should not load customElements a second time", () => { - instance.maybeLoadCustomElement({ customElements: new Map() }); - instance.maybeLoadCustomElement({ - customElements: new Map([["remote-text", true]]), - }); - - assert.calledOnce(scriptloaderStub.loadSubScript); - }); - }); - describe("#toggleWhatsNewPref", () => { - it("should call Services.prefs.setBoolPref() with the opposite value", () => { - let checkbox = {}; - let event = { target: checkbox }; - // checkbox starts false - checkbox.checked = false; - - // toggling the checkbox to set the value to true; - // Preferences.set() gets called before the checkbox changes, - // so we have to call it with the opposite value. - instance.toggleWhatsNewPref(event); - - assert.calledOnce(setBoolPrefStub); - assert.calledWith( - setBoolPrefStub, - "browser.messaging-system.whatsNewPanel.enabled", - !checkbox.checked - ); - }); - it("should report telemetry with the opposite value", () => { - let sendUserEventTelemetryStub = sandbox.stub( - instance, - "sendUserEventTelemetry" - ); - let event = { - target: { checked: true, ownerGlobal: fakeWindow }, - }; - - instance.toggleWhatsNewPref(event); - - assert.calledOnce(sendUserEventTelemetryStub); - const { args } = sendUserEventTelemetryStub.firstCall; - assert.equal(args[1], "WNP_PREF_TOGGLE"); - assert.propertyVal(args[3].value, "prefValue", false); - }); - }); - describe("#enableAppmenuButton", () => { - it("should registerCallback on enableAppmenuButton() if there are messages", async () => { - await instance.init(waitForInitializedStub, { - getMessages: sandbox.stub().resolves([{}, {}]), - }); - // init calls `enableAppmenuButton` - everyWindowStub.registerCallback.resetHistory(); - - await instance.enableAppmenuButton(); - - assert.calledOnce(everyWindowStub.registerCallback); - assert.calledWithExactly( - everyWindowStub.registerCallback, - "appMenu-whatsnew-button", - sinon.match.func, - sinon.match.func - ); - }); - it("should not registerCallback on enableAppmenuButton() if there are no messages", async () => { - instance.init(waitForInitializedStub, { - getMessages: sandbox.stub().resolves([]), - }); - // init calls `enableAppmenuButton` - everyWindowStub.registerCallback.resetHistory(); - - await instance.enableAppmenuButton(); - - assert.notCalled(everyWindowStub.registerCallback); - }); - }); - describe("#disableAppmenuButton", () => { - it("should call the unregisterCallback", () => { - assert.notCalled(everyWindowStub.unregisterCallback); - - instance.disableAppmenuButton(); - - assert.calledOnce(everyWindowStub.unregisterCallback); - assert.calledWithExactly( - everyWindowStub.unregisterCallback, - "appMenu-whatsnew-button" - ); - }); - }); - describe("#enableToolbarButton", () => { - it("should registerCallback on enableToolbarButton if messages.length", async () => { - await instance.init(waitForInitializedStub, { - getMessages: sandbox.stub().resolves([{}, {}]), - }); - // init calls `enableAppmenuButton` - everyWindowStub.registerCallback.resetHistory(); - - await instance.enableToolbarButton(); - - assert.calledOnce(everyWindowStub.registerCallback); - assert.calledWithExactly( - everyWindowStub.registerCallback, - "whats-new-menu-button", - sinon.match.func, - sinon.match.func - ); - }); - it("should not registerCallback on enableToolbarButton if no messages", async () => { - await instance.init(waitForInitializedStub, { - getMessages: sandbox.stub().resolves([]), - }); - - await instance.enableToolbarButton(); - - assert.notCalled(everyWindowStub.registerCallback); - }); - }); - describe("Show/Hide functions", () => { - it("should unhide appmenu button on _showAppmenuButton()", async () => { - await instance._showAppmenuButton(fakeWindow); - - assert.equal(fakeElementById.hidden, false); - }); - it("should hide appmenu button on _hideAppmenuButton()", () => { - instance._hideAppmenuButton(fakeWindow); - assert.equal(fakeElementById.hidden, true); - }); - it("should not do anything if the window is closed", () => { - instance._hideAppmenuButton(fakeWindow, true); - assert.notCalled(global.PanelMultiView.getViewNode); - }); - it("should not throw if the element does not exist", () => { - let fn = instance._hideAppmenuButton.bind(null, { - browser: { ownerDocument: {} }, - }); - getViewNodeStub.returns(undefined); - assert.doesNotThrow(fn); - }); - it("should unhide toolbar button on _showToolbarButton()", async () => { - await instance._showToolbarButton(fakeWindow); - - assert.equal(fakeElementById.hidden, false); - }); - it("should hide toolbar button on _hideToolbarButton()", () => { - instance._hideToolbarButton(fakeWindow); - assert.equal(fakeElementById.hidden, true); - }); - }); - describe("#renderMessages", () => { - let getMessagesStub; - beforeEach(() => { - getMessagesStub = sandbox.stub(); - instance.init(waitForInitializedStub, { - getMessages: getMessagesStub, - sendTelemetry: fakeSendTelemetry, - }); - }); - it("should have correct state", async () => { - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.template === "whatsnew_panel_message" - ); - - getMessagesStub.returns(messages); - const ev1 = sandbox.stub(); - ev1.withArgs("type").returns(1); // tracker - ev1.withArgs("count").returns(4); - const ev2 = sandbox.stub(); - ev2.withArgs("type").returns(4); // fingerprinter - ev2.withArgs("count").returns(3); - getEventsByDateRangeStub.returns([ - { getResultByName: ev1 }, - { getResultByName: ev2 }, - ]); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - assert.propertyVal(instance.state.contentArguments, "trackerCount", 4); - assert.propertyVal( - instance.state.contentArguments, - "fingerprinterCount", - 3 - ); - }); - it("should render messages to the panel on renderMessages()", async () => { - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.template === "whatsnew_panel_message" - ); - messages[0].content.link_text = { string_id: "link_text_id" }; - - getMessagesStub.returns(messages); - const ev1 = sandbox.stub(); - ev1.withArgs("type").returns(1); // tracker - ev1.withArgs("count").returns(4); - const ev2 = sandbox.stub(); - ev2.withArgs("type").returns(4); // fingerprinter - ev2.withArgs("count").returns(3); - getEventsByDateRangeStub.returns([ - { getResultByName: ev1 }, - { getResultByName: ev2 }, - ]); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - for (let message of messages) { - assert.ok( - fakeRemoteL10n.createElement.args.find( - ([, , args]) => args && args.classList === "whatsNew-message-title" - ) - ); - if (message.content.layout === "tracking-protections") { - assert.ok( - fakeRemoteL10n.createElement.args.find( - ([, , args]) => - args && args.classList === "whatsNew-message-subtitle" - ) - ); - } - if (message.id === "WHATS_NEW_FINGERPRINTER_COUNTER_72") { - assert.ok( - fakeRemoteL10n.createElement.args.find( - ([, el, args]) => el === "h2" && args.content === 3 - ) - ); - } - assert.ok( - fakeRemoteL10n.createElement.args.find( - ([, , args]) => - args && args.classList === "whatsNew-message-content" - ) - ); - } - // Call the click handler to make coverage happy. - eventListeners.mouseup(); - assert.calledOnce(global.SpecialMessageActions.handleAction); - }); - it("should clear previous messages on 2nd renderMessages()", async () => { - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.template === "whatsnew_panel_message" - ); - const removeStub = sandbox.stub(); - fakeElementById.querySelectorAll.onCall(0).returns([]); - fakeElementById.querySelectorAll - .onCall(1) - .returns([{ remove: removeStub }, { remove: removeStub }]); - - getMessagesStub.returns(messages); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - assert.calledTwice(removeStub); - }); - it("should sort based on order field value", async () => { - const messages = (await PanelTestProvider.getMessages()).filter( - m => - m.template === "whatsnew_panel_message" && - m.content.published_date === 1560969794394 - ); - - messages.forEach(m => (m.content.title = m.order)); - - getMessagesStub.returns(messages); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - // Select the title elements that are supposed to be set to the same - // value as the `order` field of the message - const titleEls = fakeRemoteL10n.createElement.args - .filter( - ([, , args]) => args && args.classList === "whatsNew-message-title" - ) - .map(([, , args]) => args.content); - assert.deepEqual(titleEls, [1, 2, 3]); - }); - it("should accept string for image attributes", async () => { - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.id === "WHATS_NEW_70_1" - ); - getMessagesStub.returns(messages); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - const imageEl = createdCustomElements.find(el => el.tagName === "img"); - assert.calledOnce(imageEl.setAttribute); - assert.calledWithExactly( - imageEl.setAttribute, - "alt", - "Firefox Send Logo" - ); - }); - it("should set state values as data-attribute", async () => { - const message = (await PanelTestProvider.getMessages()).find( - m => m.template === "whatsnew_panel_message" - ); - getMessagesStub.returns([message]); - instance.state.contentArguments = { foo: "foo", bar: "bar" }; - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - const [, , args] = fakeRemoteL10n.createElement.args.find( - ([, , elArgs]) => elArgs && elArgs.attributes - ); - assert.ok(args); - // Currently this.state.contentArguments has 8 different entries - assert.lengthOf(Object.keys(args.attributes), 8); - assert.equal( - args.attributes.searchEngineName, - defaultSearchStub.defaultEngine.name - ); - }); - it("should only render unique dates (no duplicates)", async () => { - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.template === "whatsnew_panel_message" - ); - const uniqueDates = [ - ...new Set(messages.map(m => m.content.published_date)), - ]; - getMessagesStub.returns(messages); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - const dateElements = fakeRemoteL10n.createElement.args.filter( - ([, el, args]) => - el === "p" && args.classList === "whatsNew-message-date" - ); - assert.lengthOf(dateElements, uniqueDates.length); - }); - it("should listen for panelhidden and remove the toolbar button", async () => { - getMessagesStub.returns([]); - fakeDocument.getElementById - .withArgs("customizationui-widget-panel") - .returns(null); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - assert.notCalled(fakeElementById.addEventListener); - }); - it("should attach doCommand cbs that handle user actions", async () => { - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.template === "whatsnew_panel_message" - ); - getMessagesStub.returns(messages); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - const messageEl = createdCustomElements.find( - el => - el.tagName === "div" && el.classList.includes("whatsNew-message-body") - ); - const anchorEl = createdCustomElements.find(el => el.tagName === "a"); - - assert.notCalled(global.SpecialMessageActions.handleAction); - - messageEl.doCommand(); - anchorEl.doCommand(); - - assert.calledTwice(global.SpecialMessageActions.handleAction); - }); - it("should listen for panelhidden and remove the toolbar button", async () => { - getMessagesStub.returns([]); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - assert.calledOnce(fakeElementById.addEventListener); - assert.calledWithExactly( - fakeElementById.addEventListener, - "popuphidden", - sinon.match.func, - { - once: true, - } - ); - const [, cb] = fakeElementById.addEventListener.firstCall.args; - - assert.notCalled(everyWindowStub.unregisterCallback); - - cb(); - - assert.calledOnce(everyWindowStub.unregisterCallback); - assert.calledWithExactly( - everyWindowStub.unregisterCallback, - "whats-new-menu-button" - ); - }); - describe("#IMPRESSION", () => { - it("should dispatch a IMPRESSION for messages", async () => { - // means panel is triggered from the toolbar button - fakeElementById.hasAttribute.returns(true); - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.template === "whatsnew_panel_message" - ); - getMessagesStub.returns(messages); - const spy = sandbox.spy(instance, "sendUserEventTelemetry"); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - assert.calledOnce(spy); - assert.calledOnce(fakeSendTelemetry); - assert.propertyVal( - spy.firstCall.args[2], - "id", - messages - .map(({ id }) => id) - .sort() - .join(",") - ); - }); - it("should dispatch a CLICK for clicking a message", async () => { - // means panel is triggered from the toolbar button - fakeElementById.hasAttribute.returns(true); - // Force to render the message - fakeElementById.querySelector.returns(null); - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.template === "whatsnew_panel_message" - ); - getMessagesStub.returns([messages[0]]); - const spy = sandbox.spy(instance, "sendUserEventTelemetry"); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - assert.calledOnce(spy); - assert.calledOnce(fakeSendTelemetry); - - spy.resetHistory(); - - // Message click event listener cb - eventListeners.mouseup(); - - assert.calledOnce(spy); - assert.calledWithExactly(spy, fakeWindow, "CLICK", messages[0]); - }); - it("should dispatch a IMPRESSION with toolbar_dropdown", async () => { - // means panel is triggered from the toolbar button - fakeElementById.hasAttribute.returns(true); - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.template === "whatsnew_panel_message" - ); - getMessagesStub.resolves(messages); - const spy = sandbox.spy(instance, "sendUserEventTelemetry"); - const panelPingId = messages - .map(({ id }) => id) - .sort() - .join(","); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - assert.calledOnce(spy); - assert.calledWithExactly( - spy, - fakeWindow, - "IMPRESSION", - { - id: panelPingId, - }, - { - value: { - view: "toolbar_dropdown", - }, - } - ); - assert.calledOnce(fakeSendTelemetry); - const { - args: [dispatchPayload], - } = fakeSendTelemetry.lastCall; - assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY"); - assert.propertyVal(dispatchPayload.data, "message_id", panelPingId); - assert.deepEqual(dispatchPayload.data.event_context, { - view: "toolbar_dropdown", - }); - }); - it("should dispatch a IMPRESSION with application_menu", async () => { - // means panel is triggered as a subview in the application menu - fakeElementById.hasAttribute.returns(false); - const messages = (await PanelTestProvider.getMessages()).filter( - m => m.template === "whatsnew_panel_message" - ); - getMessagesStub.resolves(messages); - const spy = sandbox.spy(instance, "sendUserEventTelemetry"); - const panelPingId = messages - .map(({ id }) => id) - .sort() - .join(","); - - await instance.renderMessages(fakeWindow, fakeDocument, "container-id"); - - assert.calledOnce(spy); - assert.calledWithExactly( - spy, - fakeWindow, - "IMPRESSION", - { - id: panelPingId, - }, - { - value: { - view: "application_menu", - }, - } - ); - assert.calledOnce(fakeSendTelemetry); - const { - args: [dispatchPayload], - } = fakeSendTelemetry.lastCall; - assert.propertyVal(dispatchPayload, "type", "TOOLBAR_PANEL_TELEMETRY"); - assert.propertyVal(dispatchPayload.data, "message_id", panelPingId); - assert.deepEqual(dispatchPayload.data.event_context, { - view: "application_menu", - }); - }); - }); - describe("#forceShowMessage", () => { - const panelSelector = "PanelUI-whatsNew-message-container"; - let removeMessagesSpy; - let renderMessagesStub; - let addEventListenerStub; - let messages; - let browser; - beforeEach(async () => { - messages = (await PanelTestProvider.getMessages()).find( - m => m.id === "WHATS_NEW_70_1" - ); - removeMessagesSpy = sandbox.spy(instance, "removeMessages"); - renderMessagesStub = sandbox.spy(instance, "renderMessages"); - addEventListenerStub = fakeElementById.addEventListener; - browser = { ownerGlobal: fakeWindow, ownerDocument: fakeDocument }; - fakeElementById.querySelectorAll.returns([fakeElementById]); - }); - it("should call removeMessages when forcing a message to show", () => { - instance.forceShowMessage(browser, messages); - - assert.calledWithExactly(removeMessagesSpy, fakeWindow, panelSelector); - }); - it("should call renderMessages when forcing a message to show", () => { - instance.forceShowMessage(browser, messages); - - assert.calledOnce(renderMessagesStub); - assert.calledWithExactly( - renderMessagesStub, - fakeWindow, - fakeDocument, - panelSelector, - { - force: true, - messages: Array.isArray(messages) ? messages : [messages], - } - ); - }); - it("should cleanup after the panel is hidden when forcing a message to show", () => { - instance.forceShowMessage(browser, messages); - - assert.calledOnce(addEventListenerStub); - assert.calledWithExactly( - addEventListenerStub, - "popuphidden", - sinon.match.func - ); - - const [, cb] = addEventListenerStub.firstCall.args; - // Reset the call count from the first `forceShowMessage` call - removeMessagesSpy.resetHistory(); - cb({ target: { ownerGlobal: fakeWindow } }); - - assert.calledOnce(removeMessagesSpy); - assert.calledWithExactly(removeMessagesSpy, fakeWindow, panelSelector); - }); - it("should exit gracefully if called before a browser exists", () => { - instance.forceShowMessage(null, messages); - assert.neverCalledWith(removeMessagesSpy, fakeWindow, panelSelector); - }); - }); - }); -}); diff --git a/browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx b/browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx index 46d5704107..c5b0d09b39 100644 --- a/browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx +++ b/browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx @@ -1,4 +1,7 @@ -import { ASRouterAdminInner } from "content-src/components/ASRouterAdmin/ASRouterAdmin"; +import { + ASRouterAdminInner, + toBinary, +} from "content-src/components/ASRouterAdmin/ASRouterAdmin"; import { ASRouterUtils } from "content-src/asrouter-utils"; import { GlobalOverrider } from "test/unit/utils"; import React from "react"; @@ -259,4 +262,43 @@ describe("ASRouterAdmin", () => { }); }); }); + describe("toBinary", () => { + // Bringing the 'fromBinary' function over from + // messagepreview to prove it works + function fromBinary(encoded) { + const binary = atob(decodeURIComponent(encoded)); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return String.fromCharCode(...new Uint16Array(bytes.buffer)); + } + + it("correctly encodes a latin string", () => { + const testString = "Hi I am a test string"; + const expectedResult = + "SABpACAASQAgAGEAbQAgAGEAIAB0AGUAcwB0ACAAcwB0AHIAaQBuAGcA"; + + const encodedResult = toBinary(testString); + + assert.equal(encodedResult, expectedResult); + + const decodedResult = fromBinary(encodedResult); + + assert.equal(decodedResult, testString); + }); + + it("correctly encodes a non-latin string", () => { + const nonLatinString = "тестовое сообщение"; + const expectedResult = "QgQ1BEEEQgQ+BDIEPgQ1BCAAQQQ+BD4EMQRJBDUEPQQ4BDUE"; + + const encodedResult = toBinary("тестовое сообщение"); + + assert.equal(encodedResult, expectedResult); + + const decodedResult = fromBinary(encodedResult); + + assert.equal(decodedResult, nonLatinString); + }); + }); }); diff --git a/browser/components/asrouter/tests/unit/unit-entry.js b/browser/components/asrouter/tests/unit/unit-entry.js index f2046a81cb..2464b02c58 100644 --- a/browser/components/asrouter/tests/unit/unit-entry.js +++ b/browser/components/asrouter/tests/unit/unit-entry.js @@ -14,7 +14,7 @@ import FxMSCommonSchema from "../../content-src/schemas/FxMSCommon.schema.json"; import { MESSAGE_TYPE_LIST, MESSAGE_TYPE_HASH, -} from "modules/ActorConstants.sys.mjs"; +} from "modules/ActorConstants.mjs"; enzyme.configure({ adapter: new Adapter() }); diff --git a/browser/components/asrouter/tests/xpcshell/head.js b/browser/components/asrouter/tests/xpcshell/head.js index 0c6cec1ac8..fa361a00b9 100644 --- a/browser/components/asrouter/tests/xpcshell/head.js +++ b/browser/components/asrouter/tests/xpcshell/head.js @@ -81,10 +81,6 @@ async function makeValidators() { "resource://testing-common/UpdateAction.schema.json", { common: true } ), - whatsnew_panel_message: await schemaValidatorFor( - "resource://testing-common/WhatsNewMessage.schema.json", - { common: true } - ), feature_callout: await schemaValidatorFor( // For now, Feature Callout and Spotlight share a common schema "resource://testing-common/Spotlight.schema.json", diff --git a/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js b/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js index 3523355659..7e9892a595 100644 --- a/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js +++ b/browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js @@ -22,7 +22,6 @@ add_task(async function test_PanelTestProvider() { cfr_doorhanger: 1, milestone_message: 0, update_action: 1, - whatsnew_panel_message: 7, spotlight: 3, feature_callout: 1, pb_newtab: 2, diff --git a/browser/components/asrouter/yamscripts.yml b/browser/components/asrouter/yamscripts.yml index de16c269a4..e52063c911 100644 --- a/browser/components/asrouter/yamscripts.yml +++ b/browser/components/asrouter/yamscripts.yml @@ -19,6 +19,7 @@ scripts: lint: =>lint build: =>bundle:admin unit: karma start karma.mc.config.js + import: =>import-rollouts tddmc: karma start karma.mc.config.js --tdd diff --git a/browser/components/backup/.eslintrc.js b/browser/components/backup/.eslintrc.js deleted file mode 100644 index 9aafb4a214..0000000000 --- a/browser/components/backup/.eslintrc.js +++ /dev/null @@ -1,9 +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/. */ - -"use strict"; - -module.exports = { - extends: ["plugin:mozilla/require-jsdoc"], -}; diff --git a/browser/components/backup/BackupResources.sys.mjs b/browser/components/backup/BackupResources.sys.mjs index 276fabefdf..ce7f53b10d 100644 --- a/browser/components/backup/BackupResources.sys.mjs +++ b/browser/components/backup/BackupResources.sys.mjs @@ -2,14 +2,28 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -// Remove this import after BackupResource is referenced elsewhere. -// eslint-disable-next-line no-unused-vars -import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; - /** * Classes exported here are registered as a resource that can be * backed up and restored in the BackupService. * * They must extend the BackupResource base class. */ -export {}; +import { AddonsBackupResource } from "resource:///modules/backup/AddonsBackupResource.sys.mjs"; +import { CookiesBackupResource } from "resource:///modules/backup/CookiesBackupResource.sys.mjs"; +import { CredentialsAndSecurityBackupResource } from "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs"; +import { FormHistoryBackupResource } from "resource:///modules/backup/FormHistoryBackupResource.sys.mjs"; +import { MiscDataBackupResource } from "resource:///modules/backup/MiscDataBackupResource.sys.mjs"; +import { PlacesBackupResource } from "resource:///modules/backup/PlacesBackupResource.sys.mjs"; +import { PreferencesBackupResource } from "resource:///modules/backup/PreferencesBackupResource.sys.mjs"; +import { SessionStoreBackupResource } from "resource:///modules/backup/SessionStoreBackupResource.sys.mjs"; + +export { + AddonsBackupResource, + CookiesBackupResource, + CredentialsAndSecurityBackupResource, + FormHistoryBackupResource, + MiscDataBackupResource, + PlacesBackupResource, + PreferencesBackupResource, + SessionStoreBackupResource, +}; diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs index 853f4768ce..3521f315fd 100644 --- a/browser/components/backup/BackupService.sys.mjs +++ b/browser/components/backup/BackupService.sys.mjs @@ -2,7 +2,7 @@ * 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 * as BackupResources from "resource:///modules/backup/BackupResources.sys.mjs"; +import * as DefaultBackupResources from "resource:///modules/backup/BackupResources.sys.mjs"; const lazy = {}; @@ -37,6 +37,13 @@ export class BackupService { #resources = new Map(); /** + * True if a backup is currently in progress. + * + * @type {boolean} + */ + #backupInProgress = false; + + /** * Returns a reference to a BackupService singleton. If this is the first time * that this getter is accessed, this causes the BackupService singleton to be * be instantiated. @@ -48,27 +55,130 @@ export class BackupService { if (this.#instance) { return this.#instance; } - this.#instance = new BackupService(BackupResources); + this.#instance = new BackupService(DefaultBackupResources); this.#instance.takeMeasurements(); return this.#instance; } /** + * Returns a reference to the BackupService singleton. If the singleton has + * not been initialized, an error is thrown. + * + * @static + * @returns {BackupService} + */ + static get() { + if (!this.#instance) { + throw new Error("BackupService not initialized"); + } + return this.#instance; + } + + /** * Create a BackupService instance. * - * @param {object} [backupResources=BackupResources] - Object containing BackupResource classes to associate with this service. + * @param {object} [backupResources=DefaultBackupResources] - Object containing BackupResource classes to associate with this service. */ - constructor(backupResources = BackupResources) { + constructor(backupResources = DefaultBackupResources) { lazy.logConsole.debug("Instantiated"); for (const resourceName in backupResources) { - let resource = BackupResources[resourceName]; + let resource = backupResources[resourceName]; this.#resources.set(resource.key, resource); } } /** + * Create a backup of the user's profile. + * + * @param {object} [options] + * Options for the backup. + * @param {string} [options.profilePath=PathUtils.profileDir] + * The path to the profile to backup. By default, this is the current + * profile. + * @returns {Promise<undefined>} + */ + async createBackup({ profilePath = PathUtils.profileDir } = {}) { + // createBackup does not allow re-entry or concurrent backups. + if (this.#backupInProgress) { + lazy.logConsole.warn("Backup attempt already in progress"); + return; + } + + this.#backupInProgress = true; + + try { + lazy.logConsole.debug(`Creating backup for profile at ${profilePath}`); + + // First, check to see if a `backups` directory already exists in the + // profile. + let backupDirPath = PathUtils.join(profilePath, "backups"); + lazy.logConsole.debug("Creating backups folder"); + + // ignoreExisting: true is the default, but we're being explicit that it's + // okay if this folder already exists. + await IOUtils.makeDirectory(backupDirPath, { ignoreExisting: true }); + + let stagingPath = await this.#prepareStagingFolder(backupDirPath); + + // Perform the backup for each resource. + for (let resourceClass of this.#resources.values()) { + try { + lazy.logConsole.debug( + `Backing up resource with key ${resourceClass.key}. ` + + `Requires encryption: ${resourceClass.requiresEncryption}` + ); + let resourcePath = PathUtils.join(stagingPath, resourceClass.key); + await IOUtils.makeDirectory(resourcePath); + + // `backup` on each BackupResource should return us a ManifestEntry + // that we eventually write to a JSON manifest file, but for now, + // we're just going to log it. + let manifestEntry = await new resourceClass().backup( + resourcePath, + profilePath + ); + lazy.logConsole.debug( + `Backup of resource with key ${resourceClass.key} completed`, + manifestEntry + ); + } catch (e) { + lazy.logConsole.error( + `Failed to backup resource: ${resourceClass.key}`, + e + ); + } + } + } finally { + this.#backupInProgress = false; + } + } + + /** + * Constructs the staging folder for the backup in the passed in backup + * folder. If a pre-existing staging folder exists, it will be cleared out. + * + * @param {string} backupDirPath + * The path to the backup folder. + * @returns {Promise<string>} + * The path to the empty staging folder. + */ + async #prepareStagingFolder(backupDirPath) { + let stagingPath = PathUtils.join(backupDirPath, "staging"); + lazy.logConsole.debug("Checking for pre-existing staging folder"); + if (await IOUtils.exists(stagingPath)) { + // A pre-existing staging folder exists. A previous backup attempt must + // have failed or been interrupted. We'll clear it out. + lazy.logConsole.warn("A pre-existing staging folder exists. Clearing."); + await IOUtils.remove(stagingPath, { recursive: true }); + } + await IOUtils.makeDirectory(stagingPath); + + return stagingPath; + } + + /** * Take measurements of the current profile state for Telemetry. * * @returns {Promise<undefined>} @@ -97,7 +207,14 @@ export class BackupService { // Measure the size of each file we are going to backup. for (let resourceClass of this.#resources.values()) { - await new resourceClass().measure(PathUtils.profileDir); + try { + await new resourceClass().measure(PathUtils.profileDir); + } catch (e) { + lazy.logConsole.error( + `Failed to measure for resource: ${resourceClass.key}`, + e + ); + } } } } diff --git a/browser/components/backup/content/debug.html b/browser/components/backup/content/debug.html new file mode 100644 index 0000000000..5d6517cf2a --- /dev/null +++ b/browser/components/backup/content/debug.html @@ -0,0 +1,46 @@ +<!-- 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/. --> +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Profile backup debug tool</title> + + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> + </head> + <body> + <header> + <h1>Profile backup debug tool</h1> + </header> + + <main> + <section> + <h2>State</h2> + <ol> + <li> + <input + type="checkbox" + preference="browser.backup.enabled" + />BackupService component enabled + </li> + <li> + <input + type="checkbox" + preference="browser.backup.log" + />BackupService debug logging enabled + </li> + </ol> + </section> + <section id="controls"> + <h2>Controls</h2> + <button id="create-backup">Create backup</button> + <button id="open-backup-folder">Open backups folder</button> + </section> + </main> + + <script src="chrome://global/content/preferencesBindings.js"></script> + <script src="chrome://browser/content/backup/debug.js"></script> + </body> +</html> diff --git a/browser/components/backup/content/debug.js b/browser/components/backup/content/debug.js new file mode 100644 index 0000000000..fd673818c0 --- /dev/null +++ b/browser/components/backup/content/debug.js @@ -0,0 +1,59 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +/* import-globals-from /toolkit/content/preferencesBindings.js */ + +Preferences.addAll([ + { id: "browser.backup.enabled", type: "bool" }, + { id: "browser.backup.log", type: "bool" }, +]); + +const { BackupService } = ChromeUtils.importESModule( + "resource:///modules/backup/BackupService.sys.mjs" +); + +let DebugUI = { + init() { + let controls = document.querySelector("#controls"); + controls.addEventListener("click", this); + }, + + handleEvent(event) { + let target = event.target; + if (HTMLButtonElement.isInstance(event.target)) { + this.onButtonClick(target); + } + }, + + async onButtonClick(button) { + switch (button.id) { + case "create-backup": { + let service = BackupService.get(); + button.disabled = true; + await service.createBackup(); + button.disabled = false; + break; + } + case "open-backup-folder": { + let backupsDir = PathUtils.join(PathUtils.profileDir, "backups"); + + let nsLocalFile = Components.Constructor( + "@mozilla.org/file/local;1", + "nsIFile", + "initWithPath" + ); + + if (await IOUtils.exists(backupsDir)) { + new nsLocalFile(backupsDir).reveal(); + } else { + alert("backups folder doesn't exist yet"); + } + + break; + } + } + }, +}; + +DebugUI.init(); diff --git a/browser/components/backup/docs/backup-resources.rst b/browser/components/backup/docs/backup-resources.rst new file mode 100644 index 0000000000..4ead0d316d --- /dev/null +++ b/browser/components/backup/docs/backup-resources.rst @@ -0,0 +1,18 @@ +================================ +Backup Resources Reference +================================ + +A ``BackupResource`` is the base class used to represent a group of data within +a user profile that is logical to backup together. For example, the +``PlacesBackupResource`` represents both the ``places.sqlite`` SQLite database, +as well as the ``favicons.sqlite`` database. The ``AddonsBackupResource`` +represents not only the preferences for various addons, but also the XPI files +that those addons are defined in. + +Each ``BackupResource`` subclass is registered for use by the +``BackupService`` by adding it to the default set of exported classes in the +``BackupResources`` module in ``BackupResources.sys.mjs``. + +.. js:autoclass:: BackupResource + :members: + :private-members: diff --git a/browser/components/backup/docs/index.rst b/browser/components/backup/docs/index.rst index 1e201f8f1c..db9995dad2 100644 --- a/browser/components/backup/docs/index.rst +++ b/browser/components/backup/docs/index.rst @@ -11,3 +11,4 @@ into a single file that can be easily restored from. :maxdepth: 3 backup-service + backup-resources diff --git a/browser/components/backup/jar.mn b/browser/components/backup/jar.mn new file mode 100644 index 0000000000..7800962486 --- /dev/null +++ b/browser/components/backup/jar.mn @@ -0,0 +1,9 @@ +# 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/. + +browser.jar: +#ifdef NIGHTLY_BUILD + content/browser/backup/debug.html (content/debug.html) + content/browser/backup/debug.js (content/debug.js) +#endif diff --git a/browser/components/backup/metrics.yaml b/browser/components/backup/metrics.yaml index 6d6a16a178..cf6f95ee75 100644 --- a/browser/components/backup/metrics.yaml +++ b/browser/components/backup/metrics.yaml @@ -28,3 +28,279 @@ browser.backup: - mconley@mozilla.com expires: never telemetry_mirror: BROWSER_BACKUP_PROF_D_DISK_SPACE + + places_size: + type: quantity + unit: kilobyte + description: > + The total file size of the places.sqlite db located in the current profile + directory, in kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_PLACES_SIZE + + favicons_size: + type: quantity + unit: kilobyte + description: > + The total file size of the favicons.sqlite db located in the current profile + directory, in kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883642 + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_FAVICONS_SIZE + + credentials_data_size: + type: quantity + unit: kilobyte + description: > + The total size of logins, payment method, and form autofill related files + in the current profile directory, in kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_CREDENTIALS_DATA_SIZE + + security_data_size: + type: quantity + unit: kilobyte + description: > + The total size of files needed for NSS initialization parameters and security + certificate settings in the current profile directory, in kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883736 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_SECURITY_DATA_SIZE + + preferences_size: + type: quantity + unit: kilobyte + description: > + The total size of files relating to user preferences and permissions in the current profile + directory, in kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883739 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883739 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_PREFERENCES_SIZE + + misc_data_size: + type: quantity + unit: kilobyte + description: > + The total size of files for telemetry, site storage, media device origin mapping, + chrome privileged IndexedDB databases, and Mozilla Accounts in the current profile directory, + rounded to the nearest tenth kilobyte. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883747 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887746 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883747 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887746 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_MISC_DATA_SIZE + + cookies_size: + type: quantity + unit: kilobyte + description: > + The total file size of the cookies.sqlite db located in the current profile + directory, in kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_COOKIES_SIZE + + form_history_size: + type: quantity + unit: kilobyte + description: > + The file size of the formhistory.sqlite db located in the current profile + directory, in kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_FORM_HISTORY_SIZE + + session_store_backups_directory_size: + type: quantity + unit: kilobyte + description: > + The total size of the session store backups directory, in kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_SESSION_STORE_BACKUPS_DIRECTORY_SIZE + + session_store_size: + type: quantity + unit: kilobyte + description: > + The size of uncompressed session store json, in kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883740 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_SESSION_STORE_SIZE + + extensions_json_size: + type: quantity + unit: kilobyte + description: > + The total file size of the current profiles extensions metadata files, + rounded to the nearest 10 kilobytes. + Files included are: + - extensions.json + - extension-settings.json + - extension-preferences.json + - addonStartup.json.lz4 + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_EXTENSIONS_JSON_SIZE + + extension_store_permissions_data_size: + type: quantity + unit: kilobyte + description: > + The file size of the current profiles extension-store-permissions/data.safe.bin + file, rounded to the nearest 10 kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_EXTENSION_STORE_PERMISSIONS_DATA_SIZE + + storage_sync_size: + type: quantity + unit: kilobyte + description: > + The file size of the current profiles storage-sync-v2.sqlite db, + rounded to the nearest 10 kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_STORAGE_SYNC_SIZE + + browser_extension_data_size: + type: quantity + unit: kilobyte + description: > + The total size of the current profiles storage.local legacy JSON backend + in the browser-extension-data directory, rounded to the nearest 10 kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_BROWSER_EXTENSION_DATA_SIZE + + extensions_xpi_directory_size: + type: quantity + unit: kilobyte + description: > + The total size of the current profiles extensions directory, + rounded to the nearest 10 kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_EXTENSIONS_XPI_DIRECTORY_SIZE + + extensions_storage_size: + type: quantity + unit: kilobyte + description: > + The total size of all extensions storage directories, + rounded to the nearest 10 kilobytes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1883655 + data_sensitivity: + - technical + notification_emails: + - mconley@mozilla.com + expires: never + telemetry_mirror: BROWSER_BACKUP_EXTENSIONS_STORAGE_SIZE diff --git a/browser/components/backup/moz.build b/browser/components/backup/moz.build index 0ea7d66b7d..be548ce81f 100644 --- a/browser/components/backup/moz.build +++ b/browser/components/backup/moz.build @@ -7,6 +7,8 @@ with Files("**"): BUG_COMPONENT = ("Firefox", "Profiles") +JAR_MANIFESTS += ["jar.mn"] + SPHINX_TREES["docs"] = "docs" XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"] @@ -14,5 +16,13 @@ XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"] EXTRA_JS_MODULES.backup += [ "BackupResources.sys.mjs", "BackupService.sys.mjs", + "resources/AddonsBackupResource.sys.mjs", "resources/BackupResource.sys.mjs", + "resources/CookiesBackupResource.sys.mjs", + "resources/CredentialsAndSecurityBackupResource.sys.mjs", + "resources/FormHistoryBackupResource.sys.mjs", + "resources/MiscDataBackupResource.sys.mjs", + "resources/PlacesBackupResource.sys.mjs", + "resources/PreferencesBackupResource.sys.mjs", + "resources/SessionStoreBackupResource.sys.mjs", ] diff --git a/browser/components/backup/resources/AddonsBackupResource.sys.mjs b/browser/components/backup/resources/AddonsBackupResource.sys.mjs new file mode 100644 index 0000000000..83b97ed2f2 --- /dev/null +++ b/browser/components/backup/resources/AddonsBackupResource.sys.mjs @@ -0,0 +1,100 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +/** + * Backup for addons and extensions files and data. + */ +export class AddonsBackupResource extends BackupResource { + static get key() { + return "addons"; + } + + static get requiresEncryption() { + return false; + } + + async measure(profilePath = PathUtils.profileDir) { + // Report the total size of the extension json files. + const jsonFiles = [ + "extensions.json", + "extension-settings.json", + "extension-preferences.json", + "addonStartup.json.lz4", + ]; + let extensionsJsonSize = 0; + for (const filePath of jsonFiles) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + extensionsJsonSize += resourceSize; + } + } + Glean.browserBackup.extensionsJsonSize.set(extensionsJsonSize); + + // Report the size of permissions store data, if present. + let extensionStorePermissionsDataPath = PathUtils.join( + profilePath, + "extension-store-permissions", + "data.safe.bin" + ); + let extensionStorePermissionsDataSize = await BackupResource.getFileSize( + extensionStorePermissionsDataPath + ); + if (Number.isInteger(extensionStorePermissionsDataSize)) { + Glean.browserBackup.extensionStorePermissionsDataSize.set( + extensionStorePermissionsDataSize + ); + } + + // Report the size of extensions storage sync database. + let storageSyncPath = PathUtils.join(profilePath, "storage-sync-v2.sqlite"); + let storageSyncSize = await BackupResource.getFileSize(storageSyncPath); + Glean.browserBackup.storageSyncSize.set(storageSyncSize); + + // Report the total size of XPI files in the extensions directory. + let extensionsXpiDirectoryPath = PathUtils.join(profilePath, "extensions"); + let extensionsXpiDirectorySize = await BackupResource.getDirectorySize( + extensionsXpiDirectoryPath, + { + shouldExclude: (filePath, fileType) => + fileType !== "regular" || !filePath.endsWith(".xpi"), + } + ); + Glean.browserBackup.extensionsXpiDirectorySize.set( + extensionsXpiDirectorySize + ); + + // Report the total size of the browser extension data. + let browserExtensionDataPath = PathUtils.join( + profilePath, + "browser-extension-data" + ); + let browserExtensionDataSize = await BackupResource.getDirectorySize( + browserExtensionDataPath + ); + Glean.browserBackup.browserExtensionDataSize.set(browserExtensionDataSize); + + // Report the size of all moz-extension IndexedDB databases. + let defaultStoragePath = PathUtils.join(profilePath, "storage", "default"); + let extensionsStorageSize = await BackupResource.getDirectorySize( + defaultStoragePath, + { + shouldExclude: (filePath, _fileType, parentPath) => { + if ( + parentPath == defaultStoragePath && + !PathUtils.filename(filePath).startsWith("moz-extension") + ) { + return true; + } + return false; + }, + } + ); + if (Number.isInteger(extensionsStorageSize)) { + Glean.browserBackup.extensionsStorageSize.set(extensionsStorageSize); + } + } +} diff --git a/browser/components/backup/resources/BackupResource.sys.mjs b/browser/components/backup/resources/BackupResource.sys.mjs index bde3f0669c..d851eb5199 100644 --- a/browser/components/backup/resources/BackupResource.sys.mjs +++ b/browser/components/backup/resources/BackupResource.sys.mjs @@ -3,7 +3,19 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Convert from bytes to kilobytes (not kibibytes). -const BYTES_IN_KB = 1000; +export const BYTES_IN_KB = 1000; + +/** + * Convert bytes to the nearest 10th kilobyte to make the measurements fuzzier. + * + * @param {number} bytes - size in bytes. + * @returns {number} - size in kilobytes rounded to the nearest 10th kilobyte. + */ +export function bytesToFuzzyKilobytes(bytes) { + let sizeInKb = Math.ceil(bytes / BYTES_IN_KB); + let nearestTenthKb = Math.round(sizeInKb / 10) * 10; + return Math.max(nearestTenthKb, 1); +} /** * An abstract class representing a set of data within a user profile @@ -23,6 +35,21 @@ export class BackupResource { } /** + * This must be overridden to return a boolean indicating whether the + * resource requires encryption when being backed up. Encryption should be + * required for particularly sensitive data, such as passwords / credentials, + * cookies, or payment methods. If you're not sure, talk to someone from the + * Privacy team. + * + * @type {boolean} + */ + static get requiresEncryption() { + throw new Error( + "BackupResource::requiresEncryption needs to be overridden." + ); + } + + /** * Get the size of a file. * * @param {string} filePath - path to a file. @@ -40,21 +67,25 @@ export class BackupResource { return null; } - let sizeInKb = Math.ceil(size / BYTES_IN_KB); - // Make the measurement fuzzier by rounding to the nearest 10kb. - let nearestTenthKb = Math.round(sizeInKb / 10) * 10; + let nearestTenthKb = bytesToFuzzyKilobytes(size); - return Math.max(nearestTenthKb, 1); + return nearestTenthKb; } /** * Get the total size of a directory. * * @param {string} directoryPath - path to a directory. + * @param {object} options - A set of additional optional parameters. + * @param {Function} [options.shouldExclude] - an optional callback which based on file path and file type should return true + * if the file should be excluded from the computed directory size. * @returns {Promise<number|null>} - the size of all descendants of the directory in kilobytes, or null if the * directory does not exist, the path is not a directory or the size is unknown. */ - static async getDirectorySize(directoryPath) { + static async getDirectorySize( + directoryPath, + { shouldExclude = () => false } = {} + ) { if (!(await IOUtils.exists(directoryPath))) { return null; } @@ -75,15 +106,20 @@ export class BackupResource { childFilePath ); + if (shouldExclude(childFilePath, childType, directoryPath)) { + continue; + } + if (childSize >= 0) { - let sizeInKb = Math.ceil(childSize / BYTES_IN_KB); - // Make the measurement fuzzier by rounding to the nearest 10kb. - let nearestTenthKb = Math.round(sizeInKb / 10) * 10; - size += Math.max(nearestTenthKb, 1); + let nearestTenthKb = bytesToFuzzyKilobytes(childSize); + + size += nearestTenthKb; } if (childType == "directory") { - let childDirectorySize = await this.getDirectorySize(childFilePath); + let childDirectorySize = await this.getDirectorySize(childFilePath, { + shouldExclude, + }); if (Number.isInteger(childDirectorySize)) { size += childDirectorySize; } @@ -106,4 +142,29 @@ export class BackupResource { async measure(profilePath) { throw new Error("BackupResource::measure needs to be overridden."); } + + /** + * Perform a safe copy of the resource(s) and write them into the backup + * database. The Promise should resolve with an object that can be serialized + * to JSON, as it will be written to the manifest file. This same object will + * be deserialized and passed to restore() when restoring the backup. This + * object can be null if no additional information is needed to restore the + * backup. + * + * @param {string} stagingPath + * The path to the staging folder where copies of the datastores for this + * BackupResource should be written to. + * @param {string} [profilePath=null] + * This is null if the backup is being run on the currently running user + * profile. If, however, the backup is being run on a different user profile + * (for example, it's being run from a BackgroundTask on a user profile that + * just shut down, or during test), then this is a string set to that user + * profile path. + * + * @returns {Promise<object|null>} + */ + // eslint-disable-next-line no-unused-vars + async backup(stagingPath, profilePath = null) { + throw new Error("BackupResource::backup must be overridden"); + } } diff --git a/browser/components/backup/resources/CookiesBackupResource.sys.mjs b/browser/components/backup/resources/CookiesBackupResource.sys.mjs new file mode 100644 index 0000000000..8b988fd532 --- /dev/null +++ b/browser/components/backup/resources/CookiesBackupResource.sys.mjs @@ -0,0 +1,25 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +/** + * Class representing Cookies database within a user profile. + */ +export class CookiesBackupResource extends BackupResource { + static get key() { + return "cookies"; + } + + static get requiresEncryption() { + return true; + } + + async measure(profilePath = PathUtils.profileDir) { + let cookiesDBPath = PathUtils.join(profilePath, "cookies.sqlite"); + let cookiesSize = await BackupResource.getFileSize(cookiesDBPath); + + Glean.browserBackup.cookiesSize.set(cookiesSize); + } +} diff --git a/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs new file mode 100644 index 0000000000..89069de826 --- /dev/null +++ b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs @@ -0,0 +1,53 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +/** + * Class representing files needed for logins, payment methods and form autofill within a user profile. + */ +export class CredentialsAndSecurityBackupResource extends BackupResource { + static get key() { + return "credentials_and_security"; + } + + static get requiresEncryption() { + return true; + } + + async measure(profilePath = PathUtils.profileDir) { + const securityFiles = ["cert9.db", "pkcs11.txt"]; + let securitySize = 0; + + for (let filePath of securityFiles) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + securitySize += resourceSize; + } + } + + Glean.browserBackup.securityDataSize.set(securitySize); + + const credentialsFiles = [ + "key4.db", + "logins.json", + "logins-backup.json", + "autofill-profiles.json", + "credentialstate.sqlite", + "signedInUser.json", + ]; + let credentialsSize = 0; + + for (let filePath of credentialsFiles) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + credentialsSize += resourceSize; + } + } + + Glean.browserBackup.credentialsDataSize.set(credentialsSize); + } +} diff --git a/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs b/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs new file mode 100644 index 0000000000..cb314eb34d --- /dev/null +++ b/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs @@ -0,0 +1,25 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +/** + * Class representing Form history database within a user profile. + */ +export class FormHistoryBackupResource extends BackupResource { + static get key() { + return "formhistory"; + } + + static get requiresEncryption() { + return false; + } + + async measure(profilePath = PathUtils.profileDir) { + let formHistoryDBPath = PathUtils.join(profilePath, "formhistory.sqlite"); + let formHistorySize = await BackupResource.getFileSize(formHistoryDBPath); + + Glean.browserBackup.formHistorySize.set(formHistorySize); + } +} diff --git a/browser/components/backup/resources/MiscDataBackupResource.sys.mjs b/browser/components/backup/resources/MiscDataBackupResource.sys.mjs new file mode 100644 index 0000000000..97224f0e31 --- /dev/null +++ b/browser/components/backup/resources/MiscDataBackupResource.sys.mjs @@ -0,0 +1,101 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +/** + * Class representing miscellaneous files for telemetry, site storage, + * media device origin mapping, chrome privileged IndexedDB databases, + * and Mozilla Accounts within a user profile. + */ +export class MiscDataBackupResource extends BackupResource { + static get key() { + return "miscellaneous"; + } + + static get requiresEncryption() { + return false; + } + + async backup(stagingPath, profilePath = PathUtils.profileDir) { + const files = [ + "times.json", + "enumerate_devices.txt", + "SiteSecurityServiceState.bin", + ]; + + for (let fileName of files) { + let sourcePath = PathUtils.join(profilePath, fileName); + let destPath = PathUtils.join(stagingPath, fileName); + if (await IOUtils.exists(sourcePath)) { + await IOUtils.copy(sourcePath, destPath, { recursive: true }); + } + } + + const sqliteDatabases = ["protections.sqlite"]; + + for (let fileName of sqliteDatabases) { + let sourcePath = PathUtils.join(profilePath, fileName); + let destPath = PathUtils.join(stagingPath, fileName); + let connection; + + try { + connection = await lazy.Sqlite.openConnection({ + path: sourcePath, + readOnly: true, + }); + + await connection.backup(destPath); + } finally { + await connection.close(); + } + } + + // Bug 1890585 - we don't currently have the ability to copy the + // chrome-privileged IndexedDB databases under storage/permanent/chrome, so + // we'll just skip that for now. + + return null; + } + + async measure(profilePath = PathUtils.profileDir) { + const files = [ + "times.json", + "enumerate_devices.txt", + "protections.sqlite", + "SiteSecurityServiceState.bin", + ]; + + let fullSize = 0; + + for (let filePath of files) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + fullSize += resourceSize; + } + } + + let chromeIndexedDBDirPath = PathUtils.join( + profilePath, + "storage", + "permanent", + "chrome" + ); + let chromeIndexedDBDirSize = await BackupResource.getDirectorySize( + chromeIndexedDBDirPath + ); + if (Number.isInteger(chromeIndexedDBDirSize)) { + fullSize += chromeIndexedDBDirSize; + } + + Glean.browserBackup.miscDataSize.set(fullSize); + } +} diff --git a/browser/components/backup/resources/PlacesBackupResource.sys.mjs b/browser/components/backup/resources/PlacesBackupResource.sys.mjs new file mode 100644 index 0000000000..1955406f51 --- /dev/null +++ b/browser/components/backup/resources/PlacesBackupResource.sys.mjs @@ -0,0 +1,91 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "isBrowsingHistoryEnabled", + "places.history.enabled", + true +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "isSanitizeOnShutdownEnabled", + "privacy.sanitize.sanitizeOnShutdown", + false +); + +/** + * Class representing Places database related files within a user profile. + */ +export class PlacesBackupResource extends BackupResource { + static get key() { + return "places"; + } + + static get requiresEncryption() { + return false; + } + + async backup(stagingPath, profilePath = PathUtils.profileDir) { + const sqliteDatabases = ["places.sqlite", "favicons.sqlite"]; + let canBackupHistory = + !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing && + !lazy.isSanitizeOnShutdownEnabled && + lazy.isBrowsingHistoryEnabled; + + /** + * Do not backup places.sqlite and favicons.sqlite if users have history disabled, want history cleared on shutdown or are using permanent private browsing mode. + * Instead, export all existing bookmarks to a compressed JSON file that we can read when restoring the backup. + */ + if (!canBackupHistory) { + let bookmarksBackupFile = PathUtils.join( + stagingPath, + "bookmarks.jsonlz4" + ); + await lazy.BookmarkJSONUtils.exportToFile(bookmarksBackupFile, { + compress: true, + }); + return { bookmarksOnly: true }; + } + + for (let fileName of sqliteDatabases) { + let sourcePath = PathUtils.join(profilePath, fileName); + let destPath = PathUtils.join(stagingPath, fileName); + let connection; + + try { + connection = await lazy.Sqlite.openConnection({ + path: sourcePath, + readOnly: true, + }); + + await connection.backup(destPath); + } finally { + await connection.close(); + } + } + return null; + } + + async measure(profilePath = PathUtils.profileDir) { + let placesDBPath = PathUtils.join(profilePath, "places.sqlite"); + let faviconsDBPath = PathUtils.join(profilePath, "favicons.sqlite"); + let placesDBSize = await BackupResource.getFileSize(placesDBPath); + let faviconsDBSize = await BackupResource.getFileSize(faviconsDBPath); + + Glean.browserBackup.placesSize.set(placesDBSize); + Glean.browserBackup.faviconsSize.set(faviconsDBSize); + } +} diff --git a/browser/components/backup/resources/PreferencesBackupResource.sys.mjs b/browser/components/backup/resources/PreferencesBackupResource.sys.mjs new file mode 100644 index 0000000000..012c0bf91e --- /dev/null +++ b/browser/components/backup/resources/PreferencesBackupResource.sys.mjs @@ -0,0 +1,98 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; +import { Sqlite } from "resource://gre/modules/Sqlite.sys.mjs"; + +/** + * Class representing files that modify preferences and permissions within a user profile. + */ +export class PreferencesBackupResource extends BackupResource { + static get key() { + return "preferences"; + } + + static get requiresEncryption() { + return false; + } + + async backup(stagingPath, profilePath = PathUtils.profileDir) { + // These are files that can be simply copied into the staging folder using + // IOUtils.copy. + const simpleCopyFiles = [ + "xulstore.json", + "containers.json", + "handlers.json", + "search.json.mozlz4", + "user.js", + "chrome", + ]; + + for (let fileName of simpleCopyFiles) { + let sourcePath = PathUtils.join(profilePath, fileName); + let destPath = PathUtils.join(stagingPath, fileName); + if (await IOUtils.exists(sourcePath)) { + await IOUtils.copy(sourcePath, destPath, { recursive: true }); + } + } + + const sqliteDatabases = ["permissions.sqlite", "content-prefs.sqlite"]; + + for (let fileName of sqliteDatabases) { + let sourcePath = PathUtils.join(profilePath, fileName); + let destPath = PathUtils.join(stagingPath, fileName); + let connection; + + try { + connection = await Sqlite.openConnection({ + path: sourcePath, + }); + + await connection.backup(destPath); + } finally { + await connection.close(); + } + } + + // prefs.js is a special case - we have a helper function to flush the + // current prefs state to disk off of the main thread. + let prefsDestPath = PathUtils.join(stagingPath, "prefs.js"); + let prefsDestFile = await IOUtils.getFile(prefsDestPath); + await Services.prefs.backupPrefFile(prefsDestFile); + + return null; + } + + async measure(profilePath = PathUtils.profileDir) { + const files = [ + "prefs.js", + "xulstore.json", + "permissions.sqlite", + "content-prefs.sqlite", + "containers.json", + "handlers.json", + "search.json.mozlz4", + "user.js", + ]; + let fullSize = 0; + + for (let filePath of files) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + fullSize += resourceSize; + } + } + + const chromeDirectoryPath = PathUtils.join(profilePath, "chrome"); + let chromeDirectorySize = await BackupResource.getDirectorySize( + chromeDirectoryPath + ); + if (Number.isInteger(chromeDirectorySize)) { + fullSize += chromeDirectorySize; + } + + Glean.browserBackup.preferencesSize.set(fullSize); + } +} diff --git a/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs new file mode 100644 index 0000000000..fa5dcca848 --- /dev/null +++ b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs @@ -0,0 +1,53 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +import { + BackupResource, + bytesToFuzzyKilobytes, +} from "resource:///modules/backup/BackupResource.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", +}); + +/** + * Class representing Session store related files within a user profile. + */ +export class SessionStoreBackupResource extends BackupResource { + static get key() { + return "sessionstore"; + } + + static get requiresEncryption() { + // Session store data does not require encryption, but if encryption is + // disabled, then session cookies will be cleared from the backup before + // writing it to the disk. + return false; + } + + async measure(profilePath = PathUtils.profileDir) { + // Get the current state of the session store JSON and + // measure it's uncompressed size. + let sessionStoreJson = lazy.SessionStore.getCurrentState(true); + let sessionStoreSize = new TextEncoder().encode( + JSON.stringify(sessionStoreJson) + ).byteLength; + let sessionStoreNearestTenthKb = bytesToFuzzyKilobytes(sessionStoreSize); + + Glean.browserBackup.sessionStoreSize.set(sessionStoreNearestTenthKb); + + let sessionStoreBackupsDirectoryPath = PathUtils.join( + profilePath, + "sessionstore-backups" + ); + let sessionStoreBackupsDirectorySize = + await BackupResource.getDirectorySize(sessionStoreBackupsDirectoryPath); + + Glean.browserBackup.sessionStoreBackupsDirectorySize.set( + sessionStoreBackupsDirectorySize + ); + } +} diff --git a/browser/components/backup/tests/xpcshell/head.js b/browser/components/backup/tests/xpcshell/head.js new file mode 100644 index 0000000000..2402870a13 --- /dev/null +++ b/browser/components/backup/tests/xpcshell/head.js @@ -0,0 +1,167 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { BackupService } = ChromeUtils.importESModule( + "resource:///modules/backup/BackupService.sys.mjs" +); + +const { BackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/BackupResource.sys.mjs" +); + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const { Sqlite } = ChromeUtils.importESModule( + "resource://gre/modules/Sqlite.sys.mjs" +); + +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + +const BYTES_IN_KB = 1000; + +do_get_profile(); + +/** + * Some fake backup resource classes to test with. + */ +class FakeBackupResource1 extends BackupResource { + static get key() { + return "fake1"; + } + static get requiresEncryption() { + return false; + } +} + +/** + * Another fake backup resource class to test with. + */ +class FakeBackupResource2 extends BackupResource { + static get key() { + return "fake2"; + } + static get requiresEncryption() { + return true; + } +} + +/** + * Yet another fake backup resource class to test with. + */ +class FakeBackupResource3 extends BackupResource { + static get key() { + return "fake3"; + } + static get requiresEncryption() { + return false; + } +} + +/** + * Create a file of a given size in kilobytes. + * + * @param {string} path the path where the file will be created. + * @param {number} sizeInKB size file in Kilobytes. + * @returns {Promise<undefined>} + */ +async function createKilobyteSizedFile(path, sizeInKB) { + let bytes = new Uint8Array(sizeInKB * BYTES_IN_KB); + await IOUtils.write(path, bytes); +} + +/** + * @typedef {object} TestFileObject + * @property {(string|Array.<string>)} path + * The relative path of the file. It can be a string or an array of strings + * in the event that directories need to be created. For example, this is + * an array of valid TestFileObjects. + * + * [ + * { path: "file1.txt" }, + * { path: ["dir1", "file2.txt"] }, + * { path: ["dir2", "dir3", "file3.txt"], sizeInKB: 25 }, + * { path: "file4.txt" }, + * ] + * + * @property {number} [sizeInKB=10] + * The size of the created file in kilobytes. Defaults to 10. + */ + +/** + * Easily creates a series of test files and directories under parentPath. + * + * @param {string} parentPath + * The path to the parent directory where the files will be created. + * @param {TestFileObject[]} testFilesArray + * An array of TestFileObjects describing what test files to create within + * the parentPath. + * @see TestFileObject + * @returns {Promise<undefined>} + */ +async function createTestFiles(parentPath, testFilesArray) { + for (let { path, sizeInKB } of testFilesArray) { + if (Array.isArray(path)) { + // Make a copy of the array of path elements, chopping off the last one. + // We'll assume the unchopped items are directories, and make sure they + // exist first. + let folders = path.slice(0, -1); + await IOUtils.getDirectory(PathUtils.join(parentPath, ...folders)); + } + + if (sizeInKB === undefined) { + sizeInKB = 10; + } + + // This little piece of cleverness coerces a string into an array of one + // if path is a string, or just leaves it alone if it's already an array. + let filePath = PathUtils.join(parentPath, ...[].concat(path)); + await createKilobyteSizedFile(filePath, sizeInKB); + } +} + +/** + * Checks that files exist within a particular folder. The filesize is not + * checked. + * + * @param {string} parentPath + * The path to the parent directory where the files should exist. + * @param {TestFileObject[]} testFilesArray + * An array of TestFileObjects describing what test files to search for within + * parentPath. + * @see TestFileObject + * @returns {Promise<undefined>} + */ +async function assertFilesExist(parentPath, testFilesArray) { + for (let { path } of testFilesArray) { + let copiedFileName = PathUtils.join(parentPath, ...[].concat(path)); + Assert.ok( + await IOUtils.exists(copiedFileName), + `${copiedFileName} should exist in the staging folder` + ); + } +} + +/** + * Remove a file or directory at a path if it exists and files are unlocked. + * + * @param {string} path path to remove. + */ +async function maybeRemovePath(path) { + try { + await IOUtils.remove(path, { ignoreAbsent: true, recursive: true }); + } catch (error) { + // Sometimes remove() throws when the file is not unlocked soon + // enough. + if (error.name != "NS_ERROR_FILE_IS_LOCKED") { + // Ignoring any errors, as the temp folder will be cleaned up. + console.error(error); + } + } +} diff --git a/browser/components/backup/tests/xpcshell/test_BrowserResource.js b/browser/components/backup/tests/xpcshell/test_BackupResource.js index 23c8e077a5..6623f4cd77 100644 --- a/browser/components/backup/tests/xpcshell/test_BrowserResource.js +++ b/browser/components/backup/tests/xpcshell/test_BackupResource.js @@ -3,16 +3,12 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -const { BackupResource } = ChromeUtils.importESModule( +const { bytesToFuzzyKilobytes } = ChromeUtils.importESModule( "resource:///modules/backup/BackupResource.sys.mjs" ); const EXPECTED_KILOBYTES_FOR_XULSTORE = 1; -add_setup(() => { - do_get_profile(); -}); - /** * Tests that BackupService.getFileSize will get the size of a file in kilobytes. */ @@ -35,7 +31,7 @@ add_task(async function test_getFileSize() { }); /** - * Tests that BackupService.getFileSize will get the total size of all the files in a directory and it's children in kilobytes. + * Tests that BackupService.getDirectorySize will get the total size of all the files in a directory and it's children in kilobytes. */ add_task(async function test_getDirectorySize() { let file = do_get_file("data/test_xulstore.json"); @@ -61,3 +57,21 @@ add_task(async function test_getDirectorySize() { await IOUtils.remove(testDir, { recursive: true }); }); + +/** + * Tests that bytesToFuzzyKilobytes will convert bytes to kilobytes + * and round up to the nearest tenth kilobyte. + */ +add_task(async function test_bytesToFuzzyKilobytes() { + let largeSize = bytesToFuzzyKilobytes(1234000); + + Assert.equal( + largeSize, + 1230, + "1234 bytes is rounded up to the nearest tenth kilobyte, 1230" + ); + + let smallSize = bytesToFuzzyKilobytes(3); + + Assert.equal(smallSize, 1, "Sizes under 10 kilobytes return 1 kilobyte"); +}); diff --git a/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js b/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js new file mode 100644 index 0000000000..e57dd50cd3 --- /dev/null +++ b/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { MiscDataBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/MiscDataBackupResource.sys.mjs" +); + +/** + * Tests that we can measure miscellaneous files in the profile directory. + */ +add_task(async function test_measure() { + Services.fog.testResetFOG(); + + const EXPECTED_MISC_KILOBYTES_SIZE = 241; + const tempDir = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "MiscDataBackupResource-measurement-test" + ); + + const mockFiles = [ + { path: "times.json", sizeInKB: 5 }, + { path: "enumerate_devices.txt", sizeInKB: 1 }, + { path: "protections.sqlite", sizeInKB: 100 }, + { path: "SiteSecurityServiceState.bin", sizeInKB: 10 }, + { path: ["storage", "permanent", "chrome", "123ABC.sqlite"], sizeInKB: 40 }, + { path: ["storage", "permanent", "chrome", "456DEF.sqlite"], sizeInKB: 40 }, + { + path: ["storage", "permanent", "chrome", "mockIDBDir", "890HIJ.sqlite"], + sizeInKB: 40, + }, + ]; + + await createTestFiles(tempDir, mockFiles); + + let miscDataBackupResource = new MiscDataBackupResource(); + await miscDataBackupResource.measure(tempDir); + + let measurement = Glean.browserBackup.miscDataSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.misc_data_size", + measurement, + "Glean and telemetry measurements for misc data should be equal" + ); + Assert.equal( + measurement, + EXPECTED_MISC_KILOBYTES_SIZE, + "Should have collected the correct glean measurement for misc files" + ); + + await maybeRemovePath(tempDir); +}); + +add_task(async function test_backup() { + let sandbox = sinon.createSandbox(); + + let miscDataBackupResource = new MiscDataBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "MiscDataBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "MiscDataBackupResource-staging-test" + ); + + const simpleCopyFiles = [ + { path: "times.json" }, + { path: "enumerate_devices.txt" }, + { path: "SiteSecurityServiceState.bin" }, + ]; + await createTestFiles(sourcePath, simpleCopyFiles); + + // We have no need to test that Sqlite.sys.mjs's backup method is working - + // this is something that is tested in Sqlite's own tests. We can just make + // sure that it's being called using sinon. Unfortunately, we cannot do the + // same thing with IOUtils.copy, as its methods are not stubbable. + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + await miscDataBackupResource.backup(stagingPath, sourcePath); + + await assertFilesExist(stagingPath, simpleCopyFiles); + + // Next, we'll make sure that the Sqlite connection had `backup` called on it + // with the right arguments. + Assert.ok( + fakeConnection.backup.calledOnce, + "Called backup the expected number of times for all connections" + ); + Assert.ok( + fakeConnection.backup.firstCall.calledWith( + PathUtils.join(stagingPath, "protections.sqlite") + ), + "Called backup on the protections.sqlite Sqlite connection" + ); + + // Bug 1890585 - we don't currently have the ability to copy the + // chrome-privileged IndexedDB databases under storage/permanent/chrome, so + // we'll just skip testing that for now. + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); diff --git a/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js new file mode 100644 index 0000000000..de97281372 --- /dev/null +++ b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PlacesBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/PlacesBackupResource.sys.mjs" +); +const { PrivateBrowsingUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" +); + +const HISTORY_ENABLED_PREF = "places.history.enabled"; +const SANITIZE_ON_SHUTDOWN_PREF = "privacy.sanitize.sanitizeOnShutdown"; + +registerCleanupFunction(() => { + /** + * Even though test_backup_no_saved_history clears user prefs too, + * clear them here as well in case that test fails and we don't + * reach the end of the test, which handles the cleanup. + */ + Services.prefs.clearUserPref(HISTORY_ENABLED_PREF); + Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF); +}); + +/** + * Tests that we can measure Places DB related files in the profile directory. + */ +add_task(async function test_measure() { + Services.fog.testResetFOG(); + + const EXPECTED_PLACES_DB_SIZE = 5240; + const EXPECTED_FAVICONS_DB_SIZE = 5240; + + // Create resource files in temporary directory + const tempDir = PathUtils.tempDir; + let tempPlacesDBPath = PathUtils.join(tempDir, "places.sqlite"); + let tempFaviconsDBPath = PathUtils.join(tempDir, "favicons.sqlite"); + await createKilobyteSizedFile(tempPlacesDBPath, EXPECTED_PLACES_DB_SIZE); + await createKilobyteSizedFile(tempFaviconsDBPath, EXPECTED_FAVICONS_DB_SIZE); + + let placesBackupResource = new PlacesBackupResource(); + await placesBackupResource.measure(tempDir); + + let placesMeasurement = Glean.browserBackup.placesSize.testGetValue(); + let faviconsMeasurement = Glean.browserBackup.faviconsSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Compare glean vs telemetry measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.places_size", + placesMeasurement, + "Glean and telemetry measurements for places.sqlite should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.favicons_size", + faviconsMeasurement, + "Glean and telemetry measurements for favicons.sqlite should be equal" + ); + + // Compare glean measurements vs actual file sizes + Assert.equal( + placesMeasurement, + EXPECTED_PLACES_DB_SIZE, + "Should have collected the correct glean measurement for places.sqlite" + ); + Assert.equal( + faviconsMeasurement, + EXPECTED_FAVICONS_DB_SIZE, + "Should have collected the correct glean measurement for favicons.sqlite" + ); + + await maybeRemovePath(tempPlacesDBPath); + await maybeRemovePath(tempFaviconsDBPath); +}); + +/** + * Tests that the backup method correctly copies places.sqlite and + * favicons.sqlite from the profile directory into the staging directory. + */ +add_task(async function test_backup() { + let sandbox = sinon.createSandbox(); + + let placesBackupResource = new PlacesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-staging-test" + ); + + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.calledTwice, + "Backup should have been called twice" + ); + Assert.ok( + fakeConnection.backup.firstCall.calledWith( + PathUtils.join(stagingPath, "places.sqlite") + ), + "places.sqlite should have been backed up first" + ); + Assert.ok( + fakeConnection.backup.secondCall.calledWith( + PathUtils.join(stagingPath, "favicons.sqlite") + ), + "favicons.sqlite should have been backed up second" + ); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); + +/** + * Tests that the backup method correctly creates a compressed bookmarks JSON file when users + * don't want history saved, even on shutdown. + */ +add_task(async function test_backup_no_saved_history() { + let sandbox = sinon.createSandbox(); + + let placesBackupResource = new PlacesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-staging-test" + ); + + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + /** + * First verify that remember history pref alone affects backup file type for places, + * despite sanitize on shutdown pref value. + */ + Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, false); + Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, false); + + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.notCalled, + "No sqlite connections should have been made with remember history disabled" + ); + await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]); + await IOUtils.remove(PathUtils.join(stagingPath, "bookmarks.jsonlz4")); + + /** + * Now verify that the sanitize shutdown pref alone affects backup file type for places, + * even if the user is okay with remembering history while browsing. + */ + Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, true); + Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, true); + + fakeConnection.backup.resetHistory(); + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.notCalled, + "No sqlite connections should have been made with sanitize shutdown enabled" + ); + await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); + Services.prefs.clearUserPref(HISTORY_ENABLED_PREF); + Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF); +}); + +/** + * Tests that the backup method correctly creates a compressed bookmarks JSON file when + * permanent private browsing mode is enabled. + */ +add_task(async function test_backup_private_browsing() { + let sandbox = sinon.createSandbox(); + + let placesBackupResource = new PlacesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-staging-test" + ); + + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + sandbox.stub(PrivateBrowsingUtils, "permanentPrivateBrowsing").value(true); + + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.notCalled, + "No sqlite connections should have been made with permanent private browsing enabled" + ); + await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); diff --git a/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js new file mode 100644 index 0000000000..6845431bb8 --- /dev/null +++ b/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js @@ -0,0 +1,132 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PreferencesBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/PreferencesBackupResource.sys.mjs" +); + +/** + * Test that the measure method correctly collects the disk-sizes of things that + * the PreferencesBackupResource is meant to back up. + */ +add_task(async function test_measure() { + Services.fog.testResetFOG(); + + const EXPECTED_PREFERENCES_KILOBYTES_SIZE = 415; + const tempDir = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PreferencesBackupResource-measure-test" + ); + const mockFiles = [ + { path: "prefs.js", sizeInKB: 20 }, + { path: "xulstore.json", sizeInKB: 1 }, + { path: "permissions.sqlite", sizeInKB: 100 }, + { path: "content-prefs.sqlite", sizeInKB: 260 }, + { path: "containers.json", sizeInKB: 1 }, + { path: "handlers.json", sizeInKB: 1 }, + { path: "search.json.mozlz4", sizeInKB: 1 }, + { path: "user.js", sizeInKB: 2 }, + { path: ["chrome", "userChrome.css"], sizeInKB: 5 }, + { path: ["chrome", "userContent.css"], sizeInKB: 5 }, + { path: ["chrome", "css", "mockStyles.css"], sizeInKB: 5 }, + ]; + + await createTestFiles(tempDir, mockFiles); + + let preferencesBackupResource = new PreferencesBackupResource(); + + await preferencesBackupResource.measure(tempDir); + + let measurement = Glean.browserBackup.preferencesSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.preferences_size", + measurement, + "Glean and telemetry measurements for preferences data should be equal" + ); + Assert.equal( + measurement, + EXPECTED_PREFERENCES_KILOBYTES_SIZE, + "Should have collected the correct glean measurement for preferences files" + ); + + await maybeRemovePath(tempDir); +}); + +/** + * Test that the backup method correctly copies items from the profile directory + * into the staging directory. + */ +add_task(async function test_backup() { + let sandbox = sinon.createSandbox(); + + let preferencesBackupResource = new PreferencesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PreferencesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PreferencesBackupResource-staging-test" + ); + + const simpleCopyFiles = [ + { path: "xulstore.json" }, + { path: "containers.json" }, + { path: "handlers.json" }, + { path: "search.json.mozlz4" }, + { path: "user.js" }, + { path: ["chrome", "userChrome.css"] }, + { path: ["chrome", "userContent.css"] }, + { path: ["chrome", "childFolder", "someOtherStylesheet.css"] }, + ]; + await createTestFiles(sourcePath, simpleCopyFiles); + + // We have no need to test that Sqlite.sys.mjs's backup method is working - + // this is something that is tested in Sqlite's own tests. We can just make + // sure that it's being called using sinon. Unfortunately, we cannot do the + // same thing with IOUtils.copy, as its methods are not stubbable. + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + await preferencesBackupResource.backup(stagingPath, sourcePath); + + await assertFilesExist(stagingPath, simpleCopyFiles); + + // Next, we'll make sure that the Sqlite connection had `backup` called on it + // with the right arguments. + Assert.ok( + fakeConnection.backup.calledTwice, + "Called backup the expected number of times for all connections" + ); + Assert.ok( + fakeConnection.backup.firstCall.calledWith( + PathUtils.join(stagingPath, "permissions.sqlite") + ), + "Called backup on the permissions.sqlite Sqlite connection" + ); + Assert.ok( + fakeConnection.backup.secondCall.calledWith( + PathUtils.join(stagingPath, "content-prefs.sqlite") + ), + "Called backup on the content-prefs.sqlite Sqlite connection" + ); + + // And we'll make sure that preferences were properly written out. + Assert.ok( + await IOUtils.exists(PathUtils.join(stagingPath, "prefs.js")), + "prefs.js should exist in the staging folder" + ); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); diff --git a/browser/components/backup/tests/xpcshell/test_createBackup.js b/browser/components/backup/tests/xpcshell/test_createBackup.js new file mode 100644 index 0000000000..fcace695ef --- /dev/null +++ b/browser/components/backup/tests/xpcshell/test_createBackup.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that calling BackupService.createBackup will call backup on each + * registered BackupResource, and that each BackupResource will have a folder + * created for them to write into. + */ +add_task(async function test_createBackup() { + let sandbox = sinon.createSandbox(); + sandbox + .stub(FakeBackupResource1.prototype, "backup") + .resolves({ fake1: "hello from 1" }); + sandbox + .stub(FakeBackupResource2.prototype, "backup") + .rejects(new Error("Some failure to backup")); + sandbox + .stub(FakeBackupResource3.prototype, "backup") + .resolves({ fake3: "hello from 3" }); + + let bs = new BackupService({ + FakeBackupResource1, + FakeBackupResource2, + FakeBackupResource3, + }); + + let fakeProfilePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "createBackupTest" + ); + + await bs.createBackup({ profilePath: fakeProfilePath }); + + // For now, we expect a staging folder to exist under the fakeProfilePath, + // and we should find a folder for each fake BackupResource. + let stagingPath = PathUtils.join(fakeProfilePath, "backups", "staging"); + Assert.ok(await IOUtils.exists(stagingPath), "Staging folder exists"); + + for (let backupResourceClass of [ + FakeBackupResource1, + FakeBackupResource2, + FakeBackupResource3, + ]) { + let expectedResourceFolder = PathUtils.join( + stagingPath, + backupResourceClass.key + ); + Assert.ok( + await IOUtils.exists(expectedResourceFolder), + `BackupResource staging folder exists for ${backupResourceClass.key}` + ); + Assert.ok( + backupResourceClass.prototype.backup.calledOnce, + `Backup was called for ${backupResourceClass.key}` + ); + Assert.ok( + backupResourceClass.prototype.backup.calledWith( + expectedResourceFolder, + fakeProfilePath + ), + `Backup was passed the right paths for ${backupResourceClass.key}` + ); + } + + // After createBackup is more fleshed out, we're going to want to make sure + // that we're writing the manifest file and that it contains the expected + // ManifestEntry objects, and that the staging folder was successfully + // renamed with the current date. + await IOUtils.remove(fakeProfilePath, { recursive: true }); + + sandbox.restore(); +}); diff --git a/browser/components/backup/tests/xpcshell/test_measurements.js b/browser/components/backup/tests/xpcshell/test_measurements.js index e5726126b2..0dece6b370 100644 --- a/browser/components/backup/tests/xpcshell/test_measurements.js +++ b/browser/components/backup/tests/xpcshell/test_measurements.js @@ -3,22 +3,59 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; -const { BackupService } = ChromeUtils.importESModule( - "resource:///modules/backup/BackupService.sys.mjs" +const { CredentialsAndSecurityBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs" +); +const { AddonsBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/AddonsBackupResource.sys.mjs" +); +const { CookiesBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/CookiesBackupResource.sys.mjs" +); + +const { FormHistoryBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/FormHistoryBackupResource.sys.mjs" ); -const { TelemetryTestUtils } = ChromeUtils.importESModule( - "resource://testing-common/TelemetryTestUtils.sys.mjs" +const { SessionStoreBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/SessionStoreBackupResource.sys.mjs" ); add_setup(() => { - do_get_profile(); // FOG needs to be initialized in order for data to flow. Services.fog.initializeFOG(); Services.telemetry.clearScalars(); }); /** + * Tests that calling `BackupService.takeMeasurements` will call the measure + * method of all registered BackupResource classes. + */ +add_task(async function test_takeMeasurements() { + let sandbox = sinon.createSandbox(); + sandbox.stub(FakeBackupResource1.prototype, "measure").resolves(); + sandbox + .stub(FakeBackupResource2.prototype, "measure") + .rejects(new Error("Some failure to measure")); + + let bs = new BackupService({ FakeBackupResource1, FakeBackupResource2 }); + await bs.takeMeasurements(); + + for (let backupResourceClass of [FakeBackupResource1, FakeBackupResource2]) { + Assert.ok( + backupResourceClass.prototype.measure.calledOnce, + "Measure was called" + ); + Assert.ok( + backupResourceClass.prototype.measure.calledWith(PathUtils.profileDir), + "Measure was called with the profile directory argument" + ); + } + + sandbox.restore(); +}); + +/** * Tests that we can measure the disk space available in the profile directory. */ add_task(async function test_profDDiskSpace() { @@ -38,3 +75,503 @@ add_task(async function test_profDDiskSpace() { "device" ); }); + +/** + * Tests that we can measure credentials related files in the profile directory. + */ +add_task(async function test_credentialsAndSecurityBackupResource() { + Services.fog.testResetFOG(); + + const EXPECTED_CREDENTIALS_KILOBYTES_SIZE = 413; + const EXPECTED_SECURITY_KILOBYTES_SIZE = 231; + + // Create resource files in temporary directory + const tempDir = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "CredentialsAndSecurityBackupResource-measurement-test" + ); + + const mockFiles = [ + // Set up credentials files + { path: "key4.db", sizeInKB: 300 }, + { path: "logins.json", sizeInKB: 1 }, + { path: "logins-backup.json", sizeInKB: 1 }, + { path: "autofill-profiles.json", sizeInKB: 1 }, + { path: "credentialstate.sqlite", sizeInKB: 100 }, + { path: "signedInUser.json", sizeInKB: 5 }, + // Set up security files + { path: "cert9.db", sizeInKB: 230 }, + { path: "pkcs11.txt", sizeInKB: 1 }, + ]; + + await createTestFiles(tempDir, mockFiles); + + let credentialsAndSecurityBackupResource = + new CredentialsAndSecurityBackupResource(); + await credentialsAndSecurityBackupResource.measure(tempDir); + + let credentialsMeasurement = + Glean.browserBackup.credentialsDataSize.testGetValue(); + let securityMeasurement = Glean.browserBackup.securityDataSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Credentials measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.credentials_data_size", + credentialsMeasurement, + "Glean and telemetry measurements for credentials data should be equal" + ); + + Assert.equal( + credentialsMeasurement, + EXPECTED_CREDENTIALS_KILOBYTES_SIZE, + "Should have collected the correct glean measurement for credentials files" + ); + + // Security measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.security_data_size", + securityMeasurement, + "Glean and telemetry measurements for security data should be equal" + ); + Assert.equal( + securityMeasurement, + EXPECTED_SECURITY_KILOBYTES_SIZE, + "Should have collected the correct glean measurement for security files" + ); + + // Cleanup + await maybeRemovePath(tempDir); +}); + +/** + * Tests that we can measure the Cookies db in a profile directory. + */ +add_task(async function test_cookiesBackupResource() { + const EXPECTED_COOKIES_DB_SIZE = 1230; + + Services.fog.testResetFOG(); + + // Create resource files in temporary directory + let tempDir = PathUtils.tempDir; + let tempCookiesDBPath = PathUtils.join(tempDir, "cookies.sqlite"); + await createKilobyteSizedFile(tempCookiesDBPath, EXPECTED_COOKIES_DB_SIZE); + + let cookiesBackupResource = new CookiesBackupResource(); + await cookiesBackupResource.measure(tempDir); + + let cookiesMeasurement = Glean.browserBackup.cookiesSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Compare glean vs telemetry measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.cookies_size", + cookiesMeasurement, + "Glean and telemetry measurements for cookies.sqlite should be equal" + ); + + // Compare glean measurements vs actual file sizes + Assert.equal( + cookiesMeasurement, + EXPECTED_COOKIES_DB_SIZE, + "Should have collected the correct glean measurement for cookies.sqlite" + ); + + await maybeRemovePath(tempCookiesDBPath); +}); + +/** + * Tests that we can measure the Form History db in a profile directory. + */ +add_task(async function test_formHistoryBackupResource() { + const EXPECTED_FORM_HISTORY_DB_SIZE = 500; + + Services.fog.testResetFOG(); + + // Create resource files in temporary directory + let tempDir = PathUtils.tempDir; + let tempFormHistoryDBPath = PathUtils.join(tempDir, "formhistory.sqlite"); + await createKilobyteSizedFile( + tempFormHistoryDBPath, + EXPECTED_FORM_HISTORY_DB_SIZE + ); + + let formHistoryBackupResource = new FormHistoryBackupResource(); + await formHistoryBackupResource.measure(tempDir); + + let formHistoryMeasurement = + Glean.browserBackup.formHistorySize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Compare glean vs telemetry measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.form_history_size", + formHistoryMeasurement, + "Glean and telemetry measurements for formhistory.sqlite should be equal" + ); + + // Compare glean measurements vs actual file sizes + Assert.equal( + formHistoryMeasurement, + EXPECTED_FORM_HISTORY_DB_SIZE, + "Should have collected the correct glean measurement for formhistory.sqlite" + ); + + await IOUtils.remove(tempFormHistoryDBPath); +}); + +/** + * Tests that we can measure the Session Store JSON and backups directory. + */ +add_task(async function test_sessionStoreBackupResource() { + const EXPECTED_KILOBYTES_FOR_BACKUPS_DIR = 1000; + Services.fog.testResetFOG(); + + // Create the sessionstore-backups directory. + let tempDir = PathUtils.tempDir; + let sessionStoreBackupsPath = PathUtils.join( + tempDir, + "sessionstore-backups", + "restore.jsonlz4" + ); + await createKilobyteSizedFile( + sessionStoreBackupsPath, + EXPECTED_KILOBYTES_FOR_BACKUPS_DIR + ); + + let sessionStoreBackupResource = new SessionStoreBackupResource(); + await sessionStoreBackupResource.measure(tempDir); + + let sessionStoreBackupsDirectoryMeasurement = + Glean.browserBackup.sessionStoreBackupsDirectorySize.testGetValue(); + let sessionStoreMeasurement = + Glean.browserBackup.sessionStoreSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Compare glean vs telemetry measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.session_store_backups_directory_size", + sessionStoreBackupsDirectoryMeasurement, + "Glean and telemetry measurements for session store backups directory should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.session_store_size", + sessionStoreMeasurement, + "Glean and telemetry measurements for session store should be equal" + ); + + // Compare glean measurements vs actual file sizes + Assert.equal( + sessionStoreBackupsDirectoryMeasurement, + EXPECTED_KILOBYTES_FOR_BACKUPS_DIR, + "Should have collected the correct glean measurement for the sessionstore-backups directory" + ); + + // Session store measurement is from `getCurrentState`, so exact size is unknown. + Assert.greater( + sessionStoreMeasurement, + 0, + "Should have collected a measurement for the session store" + ); + + await IOUtils.remove(sessionStoreBackupsPath); +}); + +/** + * Tests that we can measure the size of all the addons & extensions data. + */ +add_task(async function test_AddonsBackupResource() { + Services.fog.testResetFOG(); + Services.telemetry.clearScalars(); + + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON = 250; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE = 500; + const EXPECTED_KILOBYTES_FOR_STORAGE_SYNC = 50; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_A = 600; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_B = 400; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C = 150; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_DIRECTORY = 1000; + const EXPECTED_KILOBYTES_FOR_EXTENSION_DATA = 100; + const EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE = 200; + + let tempDir = PathUtils.tempDir; + + // Create extensions json files (all the same size). + const extensionsFilePath = PathUtils.join(tempDir, "extensions.json"); + await createKilobyteSizedFile( + extensionsFilePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON + ); + const extensionSettingsFilePath = PathUtils.join( + tempDir, + "extension-settings.json" + ); + await createKilobyteSizedFile( + extensionSettingsFilePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON + ); + const extensionsPrefsFilePath = PathUtils.join( + tempDir, + "extension-preferences.json" + ); + await createKilobyteSizedFile( + extensionsPrefsFilePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON + ); + const addonStartupFilePath = PathUtils.join(tempDir, "addonStartup.json.lz4"); + await createKilobyteSizedFile( + addonStartupFilePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON + ); + + // Create the extension store permissions data file. + let extensionStorePermissionsDataSize = PathUtils.join( + tempDir, + "extension-store-permissions", + "data.safe.bin" + ); + await createKilobyteSizedFile( + extensionStorePermissionsDataSize, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE + ); + + // Create the storage sync database file. + let storageSyncPath = PathUtils.join(tempDir, "storage-sync-v2.sqlite"); + await createKilobyteSizedFile( + storageSyncPath, + EXPECTED_KILOBYTES_FOR_STORAGE_SYNC + ); + + // Create the extensions directory with XPI files. + let extensionsXpiAPath = PathUtils.join( + tempDir, + "extensions", + "extension-b.xpi" + ); + let extensionsXpiBPath = PathUtils.join( + tempDir, + "extensions", + "extension-a.xpi" + ); + await createKilobyteSizedFile( + extensionsXpiAPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_A + ); + await createKilobyteSizedFile( + extensionsXpiBPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_B + ); + // Should be ignored. + let extensionsXpiStagedPath = PathUtils.join( + tempDir, + "extensions", + "staged", + "staged-test-extension.xpi" + ); + let extensionsXpiTrashPath = PathUtils.join( + tempDir, + "extensions", + "trash", + "trashed-test-extension.xpi" + ); + let extensionsXpiUnpackedPath = PathUtils.join( + tempDir, + "extensions", + "unpacked-extension.xpi", + "manifest.json" + ); + await createKilobyteSizedFile( + extensionsXpiStagedPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C + ); + await createKilobyteSizedFile( + extensionsXpiTrashPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C + ); + await createKilobyteSizedFile( + extensionsXpiUnpackedPath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_XPI_C + ); + + // Create the browser extension data directory. + let browserExtensionDataPath = PathUtils.join( + tempDir, + "browser-extension-data", + "test-file" + ); + await createKilobyteSizedFile( + browserExtensionDataPath, + EXPECTED_KILOBYTES_FOR_EXTENSION_DATA + ); + + // Create the extensions storage directory. + let extensionsStoragePath = PathUtils.join( + tempDir, + "storage", + "default", + "moz-extension+++test-extension-id", + "idb", + "data.sqlite" + ); + // Other storage files that should not be counted. + let otherStoragePath = PathUtils.join( + tempDir, + "storage", + "default", + "https+++accounts.firefox.com", + "ls", + "data.sqlite" + ); + + await createKilobyteSizedFile( + extensionsStoragePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE + ); + await createKilobyteSizedFile( + otherStoragePath, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE + ); + + // Measure all the extensions data. + let extensionsBackupResource = new AddonsBackupResource(); + await extensionsBackupResource.measure(tempDir); + + let extensionsJsonSizeMeasurement = + Glean.browserBackup.extensionsJsonSize.testGetValue(); + Assert.equal( + extensionsJsonSizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_JSON * 4, // There are 4 equally sized files. + "Should have collected the correct measurement of the total size of all extensions JSON files" + ); + + let extensionStorePermissionsDataSizeMeasurement = + Glean.browserBackup.extensionStorePermissionsDataSize.testGetValue(); + Assert.equal( + extensionStorePermissionsDataSizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORE, + "Should have collected the correct measurement of the size of the extension store permissions data" + ); + + let storageSyncSizeMeasurement = + Glean.browserBackup.storageSyncSize.testGetValue(); + Assert.equal( + storageSyncSizeMeasurement, + EXPECTED_KILOBYTES_FOR_STORAGE_SYNC, + "Should have collected the correct measurement of the size of the storage sync database" + ); + + let extensionsXpiDirectorySizeMeasurement = + Glean.browserBackup.extensionsXpiDirectorySize.testGetValue(); + Assert.equal( + extensionsXpiDirectorySizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_DIRECTORY, + "Should have collected the correct measurement of the size 2 equally sized XPI files in the extensions directory" + ); + + let browserExtensionDataSizeMeasurement = + Glean.browserBackup.browserExtensionDataSize.testGetValue(); + Assert.equal( + browserExtensionDataSizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSION_DATA, + "Should have collected the correct measurement of the size of the browser extension data directory" + ); + + let extensionsStorageSizeMeasurement = + Glean.browserBackup.extensionsStorageSize.testGetValue(); + Assert.equal( + extensionsStorageSizeMeasurement, + EXPECTED_KILOBYTES_FOR_EXTENSIONS_STORAGE, + "Should have collected the correct measurement of all the extensions storage" + ); + + // Compare glean vs telemetry measurements + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.extensions_json_size", + extensionsJsonSizeMeasurement, + "Glean and telemetry measurements for extensions JSON should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.extension_store_permissions_data_size", + extensionStorePermissionsDataSizeMeasurement, + "Glean and telemetry measurements for extension store permissions data should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.storage_sync_size", + storageSyncSizeMeasurement, + "Glean and telemetry measurements for storage sync database should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.extensions_xpi_directory_size", + extensionsXpiDirectorySizeMeasurement, + "Glean and telemetry measurements for extensions directory should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.browser_extension_data_size", + browserExtensionDataSizeMeasurement, + "Glean and telemetry measurements for browser extension data should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.extensions_storage_size", + extensionsStorageSizeMeasurement, + "Glean and telemetry measurements for extensions storage should be equal" + ); + + await maybeRemovePath(tempDir); +}); + +/** + * Tests that we can handle the extension store permissions data not existing. + */ +add_task( + async function test_AddonsBackupResource_no_extension_store_permissions_data() { + Services.fog.testResetFOG(); + + let tempDir = PathUtils.tempDir; + + let extensionsBackupResource = new AddonsBackupResource(); + await extensionsBackupResource.measure(tempDir); + + let extensionStorePermissionsDataSizeMeasurement = + Glean.browserBackup.extensionStorePermissionsDataSize.testGetValue(); + Assert.equal( + extensionStorePermissionsDataSizeMeasurement, + null, + "Should NOT have collected a measurement for the missing data" + ); + } +); + +/** + * Tests that we can handle a profile with no moz-extension IndexedDB databases. + */ +add_task( + async function test_AddonsBackupResource_no_extension_storage_databases() { + Services.fog.testResetFOG(); + + let tempDir = PathUtils.tempDir; + + let extensionsBackupResource = new AddonsBackupResource(); + await extensionsBackupResource.measure(tempDir); + + let extensionsStorageSizeMeasurement = + Glean.browserBackup.extensionsStorageSize.testGetValue(); + Assert.equal( + extensionsStorageSizeMeasurement, + null, + "Should NOT have collected a measurement for the missing data" + ); + } +); diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml index fb6dcd6846..07e517f1f2 100644 --- a/browser/components/backup/tests/xpcshell/xpcshell.toml +++ b/browser/components/backup/tests/xpcshell/xpcshell.toml @@ -1,8 +1,20 @@ [DEFAULT] +head = "head.js" firefox-appdir = "browser" skip-if = ["os == 'android'"] +prefs = [ + "browser.backup.log=true", +] -["test_BrowserResource.js"] +["test_BackupResource.js"] support-files = ["data/test_xulstore.json"] +["test_MiscDataBackupResource.js"] + +["test_PlacesBackupResource.js"] + +["test_PreferencesBackupResource.js"] + +["test_createBackup.js"] + ["test_measurements.js"] diff --git a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs index 34f132a539..c710f098cb 100644 --- a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs +++ b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs @@ -42,6 +42,13 @@ XPCOMUtils.defineLazyPreferenceGetter( "A DLP agent" ); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "showBlockedResult", + "browser.contentanalysis.show_blocked_result", + true +); + /** * A class that groups browsing contexts by their top-level one. * This is necessary because if there may be a subframe that @@ -236,7 +243,7 @@ export const ContentAnalysis = { }, // nsIObserver - async observe(aSubj, aTopic, aData) { + async observe(aSubj, aTopic, _aData) { switch (aTopic) { case "quit-application-requested": { let pendingRequests = @@ -293,10 +300,10 @@ export const ContentAnalysis = { ); return; } - const operation = request.analysisType; + const analysisType = request.analysisType; // For operations that block browser interaction, show the "slow content analysis" // dialog faster - let slowTimeoutMs = this._shouldShowBlockingNotification(operation) + let slowTimeoutMs = this._shouldShowBlockingNotification(analysisType) ? this._SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS : this._SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS; let browsingContext = request.windowGlobalParent?.browsingContext; @@ -326,7 +333,7 @@ export const ContentAnalysis = { timer: lazy.setTimeout(() => { this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, { notification: this._showSlowCAMessage( - operation, + analysisType, request, resourceNameOrOperationType, browsingContext @@ -338,7 +345,7 @@ export const ContentAnalysis = { }); } break; - case "dlp-response": + case "dlp-response": { const request = aSubj.QueryInterface(Ci.nsIContentAnalysisResponse); // Cancels timer or slow message UI, // if present, and possibly presents the CA verdict. @@ -372,12 +379,14 @@ export const ContentAnalysis = { windowAndResourceNameOrOperationType.resourceNameOrOperationType, windowAndResourceNameOrOperationType.browsingContext, request.requestToken, - responseResult + responseResult, + request.cancelError ); this._showAnotherPendingDialog( windowAndResourceNameOrOperationType.browsingContext ); break; + } } }, @@ -441,7 +450,10 @@ export const ContentAnalysis = { } if (this._SHOW_NOTIFICATIONS) { - const notification = new aBrowsingContext.topChromeWindow.Notification( + let topWindow = + aBrowsingContext.topChromeWindow ?? + aBrowsingContext.embedderWindowGlobal.browsingContext.topChromeWindow; + const notification = new topWindow.Notification( this.l10n.formatValueSync("contentanalysis-notification-title"), { body: aMessage, @@ -460,10 +472,10 @@ export const ContentAnalysis = { return null; }, - _shouldShowBlockingNotification(aOperation) { + _shouldShowBlockingNotification(aAnalysisType) { return !( - aOperation == Ci.nsIContentAnalysisRequest.eFileDownloaded || - aOperation == Ci.nsIContentAnalysisRequest.ePrint + aAnalysisType == Ci.nsIContentAnalysisRequest.eFileDownloaded || + aAnalysisType == Ci.nsIContentAnalysisRequest.ePrint ); }, @@ -479,6 +491,9 @@ export const ContentAnalysis = { case Ci.nsIContentAnalysisRequest.eDroppedText: l10nId = "contentanalysis-operationtype-dropped-text"; break; + case Ci.nsIContentAnalysisRequest.eOperationPrint: + l10nId = "contentanalysis-operationtype-print"; + break; } if (!l10nId) { console.error( @@ -587,10 +602,14 @@ export const ContentAnalysis = { case Ci.nsIContentAnalysisRequest.eDroppedText: l10nId = "contentanalysis-slow-agent-dialog-body-dropped-text"; break; + case Ci.nsIContentAnalysisRequest.eOperationPrint: + l10nId = "contentanalysis-slow-agent-dialog-body-print"; + break; } if (!l10nId) { console.error( - "Unknown operationTypeForDisplay: " + aResourceNameOrOperationType + "Unknown operationTypeForDisplay: ", + aResourceNameOrOperationType ); return ""; } @@ -655,7 +674,8 @@ export const ContentAnalysis = { aResourceNameOrOperationType, aBrowsingContext, aRequestToken, - aCAResult + aCAResult, + aRequestCancelError ) { let message = null; let timeoutMs = 0; @@ -676,7 +696,7 @@ export const ContentAnalysis = { ); timeoutMs = this._RESULT_NOTIFICATION_FAST_TIMEOUT_MS; break; - case Ci.nsIContentAnalysisResponse.eWarn: + case Ci.nsIContentAnalysisResponse.eWarn: { const result = await Services.prompt.asyncConfirmEx( aBrowsingContext, Ci.nsIPromptService.MODAL_TYPE_TAB, @@ -704,7 +724,12 @@ export const ContentAnalysis = { const allow = result.get("buttonNumClicked") === 0; lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow); return null; + } case Ci.nsIContentAnalysisResponse.eBlock: + if (!lazy.showBlockedResult) { + // Don't show anything + return null; + } message = await this.l10n.formatValue("contentanalysis-block-message", { content: this._getResourceNameFromNameOrOperationType( aResourceNameOrOperationType @@ -713,13 +738,51 @@ export const ContentAnalysis = { timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS; break; case Ci.nsIContentAnalysisResponse.eUnspecified: - message = await this.l10n.formatValue("contentanalysis-error-message", { - content: this._getResourceNameFromNameOrOperationType( - aResourceNameOrOperationType - ), - }); + message = await this.l10n.formatValue( + "contentanalysis-unspecified-error-message", + { + agent: lazy.agentName, + content: this._getResourceNameFromNameOrOperationType( + aResourceNameOrOperationType + ), + } + ); timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS; break; + case Ci.nsIContentAnalysisResponse.eCanceled: + { + let messageId; + switch (aRequestCancelError) { + case Ci.nsIContentAnalysisResponse.eUserInitiated: + console.error( + "Got unexpected cancel response with eUserInitiated" + ); + return null; + case Ci.nsIContentAnalysisResponse.eNoAgent: + messageId = "contentanalysis-no-agent-connected-message"; + break; + case Ci.nsIContentAnalysisResponse.eInvalidAgentSignature: + messageId = "contentanalysis-invalid-agent-signature-message"; + break; + case Ci.nsIContentAnalysisResponse.eErrorOther: + messageId = "contentanalysis-unspecified-error-message"; + break; + default: + console.error( + "Unexpected CA cancelError value: " + aRequestCancelError + ); + messageId = "contentanalysis-unspecified-error-message"; + break; + } + message = await this.l10n.formatValue(messageId, { + agent: lazy.agentName, + content: this._getResourceNameFromNameOrOperationType( + aResourceNameOrOperationType + ), + }); + timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS; + } + break; default: throw new Error("Unexpected CA result value: " + aCAResult); } diff --git a/browser/components/contextualidentity/test/browser/browser_eme.js b/browser/components/contextualidentity/test/browser/browser_eme.js index 5b0cd8a940..1c7432382f 100644 --- a/browser/components/contextualidentity/test/browser/browser_eme.js +++ b/browser/components/contextualidentity/test/browser/browser_eme.js @@ -121,7 +121,7 @@ add_task(async function test() { // Insert the media key. await new Promise(resolve => { - session.addEventListener("message", function (event) { + session.addEventListener("message", function () { session .update(aKeyInfo.keyObj) .then(() => { diff --git a/browser/components/contextualidentity/test/browser/browser_favicon.js b/browser/components/contextualidentity/test/browser/browser_favicon.js index 8d29aff28f..c4c615077a 100644 --- a/browser/components/contextualidentity/test/browser/browser_favicon.js +++ b/browser/components/contextualidentity/test/browser/browser_favicon.js @@ -20,7 +20,7 @@ function getIconFile() { loadUsingSystemPrincipal: true, contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE_FAVICON, }, - function (inputStream, status) { + function (inputStream) { let size = inputStream.available(); gFaviconData = NetUtil.readInputStreamToString(inputStream, size); resolve(); diff --git a/browser/components/contextualidentity/test/browser/browser_forgetAPI_EME_forgetThisSite.js b/browser/components/contextualidentity/test/browser/browser_forgetAPI_EME_forgetThisSite.js index 24a4c51118..10b8474072 100644 --- a/browser/components/contextualidentity/test/browser/browser_forgetAPI_EME_forgetThisSite.js +++ b/browser/components/contextualidentity/test/browser/browser_forgetAPI_EME_forgetThisSite.js @@ -108,7 +108,7 @@ async function setupEMEKey(browser) { // Insert the EME key. await new Promise(resolve => { - session.addEventListener("message", function (event) { + session.addEventListener("message", function () { session .update(aKeyInfo.keyObj) .then(() => { diff --git a/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js b/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js index 79975fff8c..204c8eccbf 100644 --- a/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js +++ b/browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js @@ -86,11 +86,11 @@ function OpenCacheEntry(key, where, flags, lci) { CacheListener.prototype = { QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), - onCacheEntryCheck(entry) { + onCacheEntryCheck() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; }, - onCacheEntryAvailable(entry, isnew, status) { + onCacheEntryAvailable() { resolve(); }, @@ -311,7 +311,7 @@ async function test_storage_cleared() { let storeRequest = store.get(1); await new Promise(done => { - storeRequest.onsuccess = event => { + storeRequest.onsuccess = () => { let res = storeRequest.result; Assert.equal( res.userContext, diff --git a/browser/components/contextualidentity/test/browser/browser_guessusercontext.js b/browser/components/contextualidentity/test/browser/browser_guessusercontext.js index 69461e67b5..90ccba9ca4 100644 --- a/browser/components/contextualidentity/test/browser/browser_guessusercontext.js +++ b/browser/components/contextualidentity/test/browser/browser_guessusercontext.js @@ -93,7 +93,7 @@ add_task(async function test() { openURIFromExternal(HOST_EXAMPLE.spec + "?new"); is( gBrowser.selectedTab.getAttribute("usercontextid"), - "", + null, "opener flow with default user context ID forced by pref" ); }); diff --git a/browser/components/contextualidentity/test/browser/browser_middleClick.js b/browser/components/contextualidentity/test/browser/browser_middleClick.js index 9b9fbcb737..d6c0feb77e 100644 --- a/browser/components/contextualidentity/test/browser/browser_middleClick.js +++ b/browser/components/contextualidentity/test/browser/browser_middleClick.js @@ -24,7 +24,7 @@ add_task(async function () { }); info("Synthesize a mouse click and wait for a new tab..."); - let newTab = await new Promise((resolve, reject) => { + let newTab = await new Promise(resolve => { gBrowser.tabContainer.addEventListener( "TabOpen", function (openEvent) { diff --git a/browser/components/contextualidentity/test/browser/browser_serviceworkers.js b/browser/components/contextualidentity/test/browser/browser_serviceworkers.js index 4f42c55d73..e40737669e 100644 --- a/browser/components/contextualidentity/test/browser/browser_serviceworkers.js +++ b/browser/components/contextualidentity/test/browser/browser_serviceworkers.js @@ -111,7 +111,7 @@ function promiseUnregister(info) { ok(aState, "ServiceWorkerRegistration exists"); resolve(); }, - unregisterFailed(aState) { + unregisterFailed() { ok(false, "unregister should succeed"); }, }, diff --git a/browser/components/contextualidentity/test/browser/browser_windowName.js b/browser/components/contextualidentity/test/browser/browser_windowName.js index 5ba2cc0e0a..256f84a8f6 100644 --- a/browser/components/contextualidentity/test/browser/browser_windowName.js +++ b/browser/components/contextualidentity/test/browser/browser_windowName.js @@ -24,7 +24,7 @@ add_task(async function test() { }); let browser1 = gBrowser.getBrowserForTab(tab1); await BrowserTestUtils.browserLoaded(browser1); - await SpecialPowers.spawn(browser1, [], function (opts) { + await SpecialPowers.spawn(browser1, [], function () { content.window.name = "tab-1"; }); @@ -34,7 +34,7 @@ add_task(async function test() { }); let browser2 = gBrowser.getBrowserForTab(tab2); await BrowserTestUtils.browserLoaded(browser2); - await SpecialPowers.spawn(browser2, [], function (opts) { + await SpecialPowers.spawn(browser2, [], function () { content.window.name = "tab-2"; }); diff --git a/browser/components/contextualidentity/test/browser/file_set_storages.html b/browser/components/contextualidentity/test/browser/file_set_storages.html index 96c46f9062..16a16d8691 100644 --- a/browser/components/contextualidentity/test/browser/file_set_storages.html +++ b/browser/components/contextualidentity/test/browser/file_set_storages.html @@ -25,7 +25,7 @@ store.createIndex("userContext", "userContext", { unique: false }); }; - request.onsuccess = event => { + request.onsuccess = () => { let db = request.result; let transaction = db.transaction(["obj"], "readwrite"); let store = transaction.objectStore("obj"); diff --git a/browser/components/controlcenter/content/protectionsPanel.inc.xhtml b/browser/components/controlcenter/content/protectionsPanel.inc.xhtml index 707105f520..29e98c2bb2 100644 --- a/browser/components/controlcenter/content/protectionsPanel.inc.xhtml +++ b/browser/components/controlcenter/content/protectionsPanel.inc.xhtml @@ -36,8 +36,8 @@ </box> <toolbarseparator></toolbarseparator> - <html:div id="messaging-system-message-container" disabled="true"> - <!-- Messaging System Messages will render in this container --> + <html:div id="info-message-container" disabled="true"> + <!-- Info message will render in this container --> </html:div> </vbox> diff --git a/browser/components/customizableui/CustomizableUI.sys.mjs b/browser/components/customizableui/CustomizableUI.sys.mjs index 5b09402dc1..9f9bbf37dc 100644 --- a/browser/components/customizableui/CustomizableUI.sys.mjs +++ b/browser/components/customizableui/CustomizableUI.sys.mjs @@ -1454,7 +1454,7 @@ var CustomizableUIInternal = { } }, - onCustomizeEnd(aWindow) { + onCustomizeEnd() { this._clearPreviousUIState(); }, @@ -6215,7 +6215,7 @@ class OverflowableToolbar { * nsIObserver implementation starts here. */ - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { // This nsIObserver method allows us to defer initialization until after // this window has finished painting and starting up. if ( diff --git a/browser/components/customizableui/CustomizableWidgets.sys.mjs b/browser/components/customizableui/CustomizableWidgets.sys.mjs index ab95e8e7db..1f37af7963 100644 --- a/browser/components/customizableui/CustomizableWidgets.sys.mjs +++ b/browser/components/customizableui/CustomizableWidgets.sys.mjs @@ -155,7 +155,7 @@ export const CustomizableWidgets = [ panelview.panelMultiView.addEventListener("PanelMultiViewHidden", this); window.addEventListener("unload", this); }, - onViewHiding(event) { + onViewHiding() { lazy.log.debug("History view is being hidden!"); }, onPanelMultiViewHidden(event) { @@ -175,7 +175,7 @@ export const CustomizableWidgets = [ } panelMultiView.removeEventListener("PanelMultiViewHidden", this); }, - onWindowUnload(event) { + onWindowUnload() { if (this._panelMenuView) { delete this._panelMenuView; } diff --git a/browser/components/customizableui/CustomizeMode.sys.mjs b/browser/components/customizableui/CustomizeMode.sys.mjs index 5f6d01d833..7b4ee373be 100644 --- a/browser/components/customizableui/CustomizeMode.sys.mjs +++ b/browser/components/customizableui/CustomizeMode.sys.mjs @@ -28,7 +28,6 @@ ChromeUtils.defineESModuleGetters(lazy, { AddonManager: "resource://gre/modules/AddonManager.sys.mjs", BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs", - SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () { @@ -63,14 +62,14 @@ var gTab; function closeGlobalTab() { let win = gTab.ownerGlobal; if (win.gBrowser.browsers.length == 1) { - win.BrowserOpenTab(); + win.BrowserCommands.openTab(); } win.gBrowser.removeTab(gTab, { animate: true }); gTab = null; } var gTabsProgressListener = { - onLocationChange(aBrowser, aWebProgress, aRequest, aLocation, aFlags) { + onLocationChange(aBrowser, aWebProgress, aRequest, aLocation) { // Tear down customize mode when the customize mode tab loads some other page. // Customize mode will be re-entered if "about:blank" is loaded again, so // don't tear down in this case. @@ -221,7 +220,6 @@ CustomizeMode.prototype = { gTab = aTab; gTab.setAttribute("customizemode", "true"); - lazy.SessionStore.persistTabAttribute("customizemode"); if (gTab.linkedPanel) { gTab.linkedBrowser.stop(); @@ -663,7 +661,7 @@ CustomizeMode.prototype = { }); }, - async addToToolbar(aNode, aReason) { + async addToToolbar(aNode) { aNode = this._getCustomizableChildForNode(aNode); if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) { aNode = aNode.firstElementChild; @@ -1282,15 +1280,15 @@ CustomizeMode.prototype = { this._onUIChange(); }, - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) { + onWidgetMoved() { this._onUIChange(); }, - onWidgetAdded(aWidgetId, aArea, aPosition) { + onWidgetAdded() { this._onUIChange(); }, - onWidgetRemoved(aWidgetId, aArea) { + onWidgetRemoved() { this._onUIChange(); }, @@ -1649,7 +1647,7 @@ CustomizeMode.prototype = { delete this.paletteDragHandler; }, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "nsPref:changed": this._updateResetButton(); @@ -2329,7 +2327,7 @@ CustomizeMode.prototype = { } }, - _setGridDragActive(aDragOverNode, aDraggedItem, aValue) { + _setGridDragActive(aDragOverNode, aDraggedItem) { let targetArea = this._getCustomizableParent(aDragOverNode); let draggedWrapper = this.$("wrapper-" + aDraggedItem.id); let originArea = this._getCustomizableParent(draggedWrapper); @@ -2428,7 +2426,7 @@ CustomizeMode.prototype = { return aElement.closest(areas.map(a => "#" + CSS.escape(a)).join(",")); }, - _getDragOverNode(aEvent, aAreaElement, aAreaType, aDraggedItemId) { + _getDragOverNode(aEvent, aAreaElement, aAreaType) { let expectedParent = CustomizableUI.getCustomizationTarget(aAreaElement) || aAreaElement; if (!expectedParent.contains(aEvent.target)) { diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js index f99560bd42..cb32085fd7 100644 --- a/browser/components/customizableui/content/panelUI.js +++ b/browser/components/customizableui/content/panelUI.js @@ -6,7 +6,6 @@ ChromeUtils.defineESModuleGetters(this, { AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs", - ToolbarPanelHub: "resource:///modules/asrouter/ToolbarPanelHub.jsm", }); /** @@ -167,9 +166,6 @@ const PanelUI = { this.menuButton.removeEventListener("mousedown", this); this.menuButton.removeEventListener("keypress", this); CustomizableUI.removeListener(this); - if (this.whatsNewPanel) { - this.whatsNewPanel.removeEventListener("ViewShowing", this); - } }, /** @@ -303,11 +299,6 @@ const PanelUI = { case "activate": this.updateNotifications(); break; - case "ViewShowing": - if (aEvent.target == this.whatsNewPanel) { - this.onWhatsNewPanelShowing(); - } - break; } }, @@ -412,7 +403,6 @@ const PanelUI = { return; } - this.ensureWhatsNewInitialized(viewNode); this.ensurePanicViewInitialized(viewNode); let container = aAnchor.closest("panelmultiview"); @@ -497,24 +487,6 @@ const PanelUI = { }, /** - * Sets up the event listener for when the What's New panel is shown. - * - * @param {panelview} panelView The What's New panelview. - */ - ensureWhatsNewInitialized(panelView) { - if (panelView.id != "PanelUI-whatsNew" || panelView._initialized) { - return; - } - - if (!this.whatsNewPanel) { - this.whatsNewPanel = panelView; - } - - panelView._initialized = true; - panelView.addEventListener("ViewShowing", this); - }, - - /** * Adds FTL before appending the panic view markup to the main DOM. * * @param {panelview} panelView The Panic View panelview. @@ -533,17 +505,6 @@ const PanelUI = { }, /** - * When the What's New panel is showing, we fetch the messages to show. - */ - onWhatsNewPanelShowing() { - ToolbarPanelHub.renderMessages( - window, - document, - "PanelUI-whatsNew-message-container" - ); - }, - - /** * NB: The enable- and disableSingleSubviewPanelAnimations methods only * affect the hiding/showing animations of single-subview panels (tempPanel * in the showSubView method). @@ -568,7 +529,7 @@ const PanelUI = { } }, - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) { + onWidgetAfterDOMChange(aNode, aNextNode, aContainer) { if (aContainer == this.overflowFixedList) { this.updateOverflowStatus(); } @@ -601,7 +562,7 @@ const PanelUI = { } }, - _onHelpViewShow(aEvent) { + _onHelpViewShow() { // Call global menu setup function buildHelpMenu(); diff --git a/browser/components/customizableui/test/browser_1087303_button_fullscreen.js b/browser/components/customizableui/test/browser_1087303_button_fullscreen.js index f67e81b892..42f9b58370 100644 --- a/browser/components/customizableui/test/browser_1087303_button_fullscreen.js +++ b/browser/components/customizableui/test/browser_1087303_button_fullscreen.js @@ -44,7 +44,7 @@ function promiseFullscreenChange() { reject("Fullscreen change did not happen within " + 20000 + "ms"); }, 20000); - function onFullscreenChange(event) { + function onFullscreenChange() { clearTimeout(timeoutId); window.removeEventListener("fullscreen", onFullscreenChange, true); info("Fullscreen event received"); diff --git a/browser/components/customizableui/test/browser_1087303_button_preferences.js b/browser/components/customizableui/test/browser_1087303_button_preferences.js index 7db48341cb..86bc89f48e 100644 --- a/browser/components/customizableui/test/browser_1087303_button_preferences.js +++ b/browser/components/customizableui/test/browser_1087303_button_preferences.js @@ -47,7 +47,7 @@ function waitForPageLoad(aTab) { reject("Page didn't load within " + 20000 + "ms"); }, 20000); - async function onTabLoad(event) { + async function onTabLoad() { clearTimeout(timeoutId); aTab.linkedBrowser.removeEventListener("load", onTabLoad, true); info("Tab event received: load"); diff --git a/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js b/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js index 89b86dba20..5d44cb1664 100644 --- a/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js +++ b/browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js @@ -21,7 +21,7 @@ add_task(async function test_PanelMultiView_toggle_with_other_popup() { gBrowser, url: TEST_URL, }, - async function (browser) { + async function () { // 1. Open the main menu. await gCUITestUtils.openMainMenu(); diff --git a/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js b/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js index 346608dc99..b174d2bccf 100644 --- a/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js +++ b/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js @@ -34,7 +34,7 @@ add_task(async function () { "Should not be in fullscreen sizemode before we enter fullscreen." ); - BrowserFullScreen(); + BrowserCommands.fullScreen(); await TestUtils.waitForCondition(() => isFullscreenSizeMode()); ok( fullscreenButton.checked, @@ -62,7 +62,7 @@ add_task(async function () { await endCustomizing(); - BrowserFullScreen(); + BrowserCommands.fullScreen(); fullscreenButton = document.getElementById("fullscreen-button"); await TestUtils.waitForCondition(() => !isFullscreenSizeMode()); ok( diff --git a/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js index 0cf9a93341..8f2dc87e19 100644 --- a/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js +++ b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js @@ -124,18 +124,17 @@ add_task(async function disabled_button_in_panel() { button.remove(); }); -registerCleanupFunction(function () { +registerCleanupFunction(async function () { if (button && button.parentNode) { button.remove(); } if (menuButton && menuButton.parentNode) { menuButton.remove(); } - // Sadly this isn't task.jsm-enabled, so we can't wait for this to happen. But we should - // definitely close it here and hope it won't interfere with other tests. - // Of course, all the tests are meant to do this themselves, but if they fail... if (isOverflowOpen()) { + let panelHiddenPromise = promiseOverflowHidden(window); PanelUI.overflowPanel.hidePopup(); + await panelHiddenPromise; } CustomizableUI.reset(); }); diff --git a/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js b/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js index cc8842a3e8..42daab891f 100644 --- a/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js +++ b/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js @@ -21,7 +21,7 @@ add_task(async function () { let privateWindow = null; let observerWindowOpened = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic == "domwindowopened") { privateWindow = aSubject; privateWindow.addEventListener( diff --git a/browser/components/customizableui/test/browser_947914_button_newWindow.js b/browser/components/customizableui/test/browser_947914_button_newWindow.js index 591d13191e..910dd2a179 100644 --- a/browser/components/customizableui/test/browser_947914_button_newWindow.js +++ b/browser/components/customizableui/test/browser_947914_button_newWindow.js @@ -21,7 +21,7 @@ add_task(async function () { let newWindow = null; let observerWindowOpened = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic == "domwindowopened") { newWindow = aSubject; newWindow.addEventListener( diff --git a/browser/components/customizableui/test/browser_947914_button_zoomReset.js b/browser/components/customizableui/test/browser_947914_button_zoomReset.js index 7dc8299b28..c97e2f17d1 100644 --- a/browser/components/customizableui/test/browser_947914_button_zoomReset.js +++ b/browser/components/customizableui/test/browser_947914_button_zoomReset.js @@ -12,7 +12,7 @@ add_task(async function () { await BrowserTestUtils.withNewTab( { gBrowser, url: "http://example.com", waitForLoad: true }, - async function (browser) { + async function () { CustomizableUI.addWidgetToArea( "zoom-controls", CustomizableUI.AREA_FIXED_OVERFLOW_PANEL diff --git a/browser/components/customizableui/test/browser_972267_customizationchange_events.js b/browser/components/customizableui/test/browser_972267_customizationchange_events.js index 7d27b94136..fdd7236d65 100644 --- a/browser/components/customizableui/test/browser_972267_customizationchange_events.js +++ b/browser/components/customizableui/test/browser_972267_customizationchange_events.js @@ -11,7 +11,7 @@ add_task(async function () { let otherToolbox = newWindow.gNavToolbox; let handlerCalledCount = 0; - let handler = ev => { + let handler = () => { handlerCalledCount++; }; diff --git a/browser/components/customizableui/test/browser_customization_context_menus.js b/browser/components/customizableui/test/browser_customization_context_menus.js index 526b3abd1b..3f4c94fb72 100644 --- a/browser/components/customizableui/test/browser_customization_context_menus.js +++ b/browser/components/customizableui/test/browser_customization_context_menus.js @@ -171,8 +171,8 @@ add_task(async function urlbar_context() { let contextMenu = document.getElementById("toolbar-context-menu"); let shownPromise = popupShown(contextMenu); let urlBarContainer = document.getElementById("urlbar-container"); - // Need to make sure not to click within an edit field. - EventUtils.synthesizeMouse(urlBarContainer, 100, 1, { + // This clicks in the urlbar container margin, to avoid hitting the urlbar field. + EventUtils.synthesizeMouse(urlBarContainer, -2, 4, { type: "contextmenu", button: 2, }); @@ -549,7 +549,7 @@ add_task(async function custom_context_menus() { await startCustomizing(); is( widget.getAttribute("context"), - "", + null, "Should not have own context menu in the toolbar now that we're customizing." ); is( @@ -562,7 +562,7 @@ add_task(async function custom_context_menus() { simulateItemDrag(widget, panel); is( widget.getAttribute("context"), - "", + null, "Should not have own context menu when in the panel." ); is( @@ -577,7 +577,7 @@ add_task(async function custom_context_menus() { ); is( widget.getAttribute("context"), - "", + null, "Should not have own context menu when back in toolbar because we're still customizing." ); is( diff --git a/browser/components/customizableui/test/browser_editcontrols_update.js b/browser/components/customizableui/test/browser_editcontrols_update.js index 9f064e521a..1276606779 100644 --- a/browser/components/customizableui/test/browser_editcontrols_update.js +++ b/browser/components/customizableui/test/browser_editcontrols_update.js @@ -29,7 +29,7 @@ function expectCommandUpdate(count, testWindow = window) { supportsCommand(cmd) { return cmd == "cmd_delete"; }, - isCommandEnabled(cmd) { + isCommandEnabled() { if (!count) { ok(false, "unexpected update"); reject(); diff --git a/browser/components/customizableui/test/browser_open_in_lazy_tab.js b/browser/components/customizableui/test/browser_open_in_lazy_tab.js index c18de67698..696bfde69b 100644 --- a/browser/components/customizableui/test/browser_open_in_lazy_tab.js +++ b/browser/components/customizableui/test/browser_open_in_lazy_tab.js @@ -9,7 +9,7 @@ add_task(async function open_customize_mode_in_lazy_tab() { }); gCustomizeMode.setTab(tab); - is(tab.linkedPanel, "", "Tab should be lazy"); + is(tab.linkedPanel, null, "Tab should be lazy"); let title = gNavigatorBundle.getFormattedString("customizeMode.tabTitle", [ document.getElementById("bundle_brand").getString("brandShortName"), diff --git a/browser/components/customizableui/test/browser_panelUINotifications.js b/browser/components/customizableui/test/browser_panelUINotifications.js index 818fcbad39..d5f2cc0450 100644 --- a/browser/components/customizableui/test/browser_panelUINotifications.js +++ b/browser/components/customizableui/test/browser_panelUINotifications.js @@ -14,7 +14,7 @@ add_task(async function testMainActionCalled() { url: "about:blank", }; - await BrowserTestUtils.withNewTab(options, function (browser) { + await BrowserTestUtils.withNewTab(options, function () { is( PanelUI.notificationPanel.state, "closed", @@ -77,7 +77,7 @@ add_task(async function testSecondaryActionWorkflow() { url: "about:blank", }; - await BrowserTestUtils.withNewTab(options, async function (browser) { + await BrowserTestUtils.withNewTab(options, async function () { is( PanelUI.notificationPanel.state, "closed", @@ -167,7 +167,7 @@ add_task(async function testDownloadingBadge() { url: "about:blank", }; - await BrowserTestUtils.withNewTab(options, async function (browser) { + await BrowserTestUtils.withNewTab(options, async function () { let mainActionCalled = false; let mainAction = { callback: () => { @@ -225,7 +225,7 @@ add_task(async function testDownloadingBadge() { * then we display any other badges that are remaining. */ add_task(async function testInteractionWithBadges() { - await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + await BrowserTestUtils.withNewTab("about:blank", async function () { // Remove the fxa toolbar button from the navbar to ensure the notification // is displayed on the app menu button. let { CustomizableUI } = ChromeUtils.importESModule( @@ -328,7 +328,7 @@ add_task(async function testInteractionWithBadges() { * This tests that adding a badge will not dismiss any existing doorhangers. */ add_task(async function testAddingBadgeWhileDoorhangerIsShowing() { - await BrowserTestUtils.withNewTab("about:blank", function (browser) { + await BrowserTestUtils.withNewTab("about:blank", function () { is( PanelUI.notificationPanel.state, "closed", @@ -468,7 +468,7 @@ add_task(async function testMultipleBadges() { * Tests that non-badges also operate like a stack. */ add_task(async function testMultipleNonBadges() { - await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + await BrowserTestUtils.withNewTab("about:blank", async function () { is( PanelUI.notificationPanel.state, "closed", diff --git a/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js index 853c39e89f..d90f928ed9 100644 --- a/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js +++ b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js @@ -20,7 +20,7 @@ function waitForDocshellActivated() { content.document, "visibilitychange", true /* capture */, - aEvent => { + () => { return content.browsingContext.isActive; } ); diff --git a/browser/components/customizableui/test/browser_panelUINotifications_modals.js b/browser/components/customizableui/test/browser_panelUINotifications_modals.js index 87be14fcee..a3aa6d058a 100644 --- a/browser/components/customizableui/test/browser_panelUINotifications_modals.js +++ b/browser/components/customizableui/test/browser_panelUINotifications_modals.js @@ -8,10 +8,6 @@ const { AppMenuNotifications } = ChromeUtils.importESModule( ); add_task(async function testModals() { - await SpecialPowers.pushPrefEnv({ - set: [["prompts.windowPromptSubDialog", true]], - }); - is( PanelUI.notificationPanel.state, "closed", diff --git a/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js index fd75763857..edda165692 100644 --- a/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js +++ b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js @@ -15,7 +15,7 @@ add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() { url: "about:blank", }; - await BrowserTestUtils.withNewTab(options, async function (browser) { + await BrowserTestUtils.withNewTab(options, async function () { let win = await BrowserTestUtils.openNewBrowserWindow(); await SimpleTest.promiseFocus(win); let mainActionCalled = false; @@ -95,7 +95,7 @@ add_task( url: "about:blank", }; - await BrowserTestUtils.withNewTab(options, async function (browser) { + await BrowserTestUtils.withNewTab(options, async function () { let win = await BrowserTestUtils.openNewBrowserWindow(); await SimpleTest.promiseFocus(win); AppMenuNotifications.showNotification("update-manual", { callback() {} }); @@ -140,7 +140,7 @@ add_task( url: "about:blank", }; - await BrowserTestUtils.withNewTab(options, async function (browser) { + await BrowserTestUtils.withNewTab(options, async function () { let win = await BrowserTestUtils.openNewBrowserWindow(); await SimpleTest.promiseFocus(win); AppMenuNotifications.showNotification("update-manual", { callback() {} }); diff --git a/browser/components/customizableui/test/browser_switch_to_customize_mode.js b/browser/components/customizableui/test/browser_switch_to_customize_mode.js index 55e80d3517..e3988cb41e 100644 --- a/browser/components/customizableui/test/browser_switch_to_customize_mode.js +++ b/browser/components/customizableui/test/browser_switch_to_customize_mode.js @@ -18,7 +18,7 @@ add_task(async function () { await finishedCustomizing; let startedCount = 0; - let handler = e => startedCount++; + let handler = () => startedCount++; gNavToolbox.addEventListener("customizationstarting", handler); await startCustomizing(); CustomizableUI.removeWidgetFromArea("stop-reload-button"); diff --git a/browser/components/customizableui/test/browser_synced_tabs_menu.js b/browser/components/customizableui/test/browser_synced_tabs_menu.js index ff60167fea..33c8f6a845 100644 --- a/browser/components/customizableui/test/browser_synced_tabs_menu.js +++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js @@ -40,7 +40,7 @@ function updateTabsPanel() { return promiseTabsUpdated; } -// This is the mock we use for SyncedTabs.jsm - tests may override various +// This is the mock we use for SyncedTabs.sys.mjs - tests may override various // functions. let mockedInternal = { get isConfiguredToSyncTabs() { @@ -378,7 +378,7 @@ add_task(async function () { // There is a single node saying there's no tabs for the client. node = node.nextElementSibling; is(node.nodeName, "label", "node is a label"); - is(node.getAttribute("itemtype"), "", "node is neither a tab nor a client"); + is(node.getAttribute("itemtype"), null, "node is neither a tab nor a client"); node = node.nextElementSibling; is(node, null, "no more siblings"); @@ -514,7 +514,7 @@ add_task(async function () { return promise; } - showMoreButton = checkTabsPage(25, "Show More Tabs"); + showMoreButton = checkTabsPage(25, "Show more tabs"); await clickShowMoreButton(); checkTabsPage(77, null); diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js index f8c0d02a12..bc1e88ed61 100644 --- a/browser/components/customizableui/test/head.js +++ b/browser/components/customizableui/test/head.js @@ -267,7 +267,7 @@ function openAndLoadWindow(aOptions, aWaitForDelayedStartup = false) { return new Promise(resolve => { let win = OpenBrowserWindow(aOptions); if (aWaitForDelayedStartup) { - Services.obs.addObserver(function onDS(aSubject, aTopic, aData) { + Services.obs.addObserver(function onDS(aSubject) { if (aSubject != win) { return; } @@ -309,7 +309,7 @@ function promisePanelElementShown(win, aPanel) { let timeoutId = win.setTimeout(() => { reject("Panel did not show within 20 seconds."); }, 20000); - function onPanelOpen(e) { + function onPanelOpen() { aPanel.removeEventListener("popupshown", onPanelOpen); win.clearTimeout(timeoutId); resolve(); @@ -328,7 +328,7 @@ function promisePanelElementHidden(win, aPanel) { let timeoutId = win.setTimeout(() => { reject("Panel did not hide within 20 seconds."); }, 20000); - function onPanelClose(e) { + function onPanelClose() { aPanel.removeEventListener("popuphidden", onPanelClose); win.clearTimeout(timeoutId); executeSoon(resolve); @@ -352,7 +352,7 @@ function subviewShown(aSubview) { let timeoutId = win.setTimeout(() => { reject("Subview (" + aSubview.id + ") did not show within 20 seconds."); }, 20000); - function onViewShown(e) { + function onViewShown() { aSubview.removeEventListener("ViewShown", onViewShown); win.clearTimeout(timeoutId); resolve(); @@ -367,7 +367,7 @@ function subviewHidden(aSubview) { let timeoutId = win.setTimeout(() => { reject("Subview (" + aSubview.id + ") did not hide within 20 seconds."); }, 20000); - function onViewHiding(e) { + function onViewHiding() { aSubview.removeEventListener("ViewHiding", onViewHiding); win.clearTimeout(timeoutId); resolve(); @@ -406,7 +406,7 @@ function promiseTabLoadEvent(aTab, aURL) { * @return {Promise} resolved when the requisite mutation shows up. */ function promiseAttributeMutation(aNode, aAttribute, aFilterFn) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { info("waiting for mutation of attribute '" + aAttribute + "'."); let obs = new MutationObserver(mutations => { for (let mut of mutations) { diff --git a/browser/components/doh/DoHConfig.sys.mjs b/browser/components/doh/DoHConfig.sys.mjs index 5d35940d55..f9ac5f0f40 100644 --- a/browser/components/doh/DoHConfig.sys.mjs +++ b/browser/components/doh/DoHConfig.sys.mjs @@ -196,7 +196,7 @@ export const DoHConfigController = { return; } - Services.obs.addObserver(function obs(sub, top, data) { + Services.obs.addObserver(function obs() { Services.obs.removeObserver(obs, lazy.Region.REGION_TOPIC); updateRegionAndResolve(); }, lazy.Region.REGION_TOPIC); diff --git a/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js b/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js index cd4356ed3f..c41fa66abe 100644 --- a/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js +++ b/browser/components/doh/test/browser/browser_remoteSettings_newProfile.js @@ -13,7 +13,7 @@ async function setPrefAndWaitForConfigFlush(pref, value) { await configFlushedPromise; } -async function clearPrefAndWaitForConfigFlush(pref, value) { +async function clearPrefAndWaitForConfigFlush(pref) { let configFlushedPromise = DoHTestUtils.waitForConfigFlush(); Preferences.reset(pref); await configFlushedPromise; diff --git a/browser/components/enterprisepolicies/Policies.sys.mjs b/browser/components/enterprisepolicies/Policies.sys.mjs index 41fc89957c..fd15c244cc 100644 --- a/browser/components/enterprisepolicies/Policies.sys.mjs +++ b/browser/components/enterprisepolicies/Policies.sys.mjs @@ -81,23 +81,23 @@ export var Policies = { // Used for cleaning up policies. // Use the same timing that you used for setting up the policy. _cleanup: { - onBeforeAddons(manager) { + onBeforeAddons() { if (Cu.isInAutomation || isXpcshell) { console.log("_cleanup from onBeforeAddons"); clearBlockedAboutPages(); } }, - onProfileAfterChange(manager) { + onProfileAfterChange() { if (Cu.isInAutomation || isXpcshell) { console.log("_cleanup from onProfileAfterChange"); } }, - onBeforeUIStartup(manager) { + onBeforeUIStartup() { if (Cu.isInAutomation || isXpcshell) { console.log("_cleanup from onBeforeUIStartup"); } }, - onAllWindowsRestored(manager) { + onAllWindowsRestored() { if (Cu.isInAutomation || isXpcshell) { console.log("_cleanup from onAllWindowsRestored"); } @@ -112,7 +112,7 @@ export var Policies = { AllowedDomainsForApps: { onBeforeAddons(manager, param) { - Services.obs.addObserver(function (subject, topic, data) { + Services.obs.addObserver(function (subject) { let channel = subject.QueryInterface(Ci.nsIHttpChannel); if (channel.URI.host.endsWith(".google.com")) { channel.setRequestHeader("X-GoogApps-Allowed-Domains", param, true); @@ -540,6 +540,15 @@ export var Policies = { param.DenyUrlRegexList ); } + if ("AgentName" in param) { + setAndLockPref("browser.contentanalysis.agent_name", param.AgentName); + } + if ("ClientSignature" in param) { + setAndLockPref( + "browser.contentanalysis.client_signature", + param.ClientSignature + ); + } let boolPrefs = [ ["IsPerUser", "is_per_user"], ["ShowBlockedResult", "show_blocked_result"], @@ -1802,6 +1811,8 @@ export var Policies = { "places.", "pref.", "print.", + "privacy.userContext.enabled", + "privacy.userContext.ui.enabled", "signon.", "spellchecker.", "toolkit.legacyUserProfileCustomizations.stylesheets", @@ -1981,13 +1992,11 @@ export var Policies = { onBeforeAddons(manager, param) { if (param.Locked) { manager.disallowFeature("changeProxySettings"); - lazy.ProxyPolicies.configureProxySettings(param, setAndLockPref); - } else { - lazy.ProxyPolicies.configureProxySettings( - param, - PoliciesUtils.setDefaultPref - ); } + lazy.ProxyPolicies.configureProxySettings( + param, + PoliciesUtils.setDefaultPref + ); }, }, @@ -2023,6 +2032,13 @@ export var Policies = { setAndLockPref("privacy.clearOnShutdown.sessions", param); setAndLockPref("privacy.clearOnShutdown.siteSettings", param); setAndLockPref("privacy.clearOnShutdown.offlineApps", param); + setAndLockPref( + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads", + param + ); + setAndLockPref("privacy.clearOnShutdown_v2.cookiesAndStorage", param); + setAndLockPref("privacy.clearOnShutdown_v2.cache", param); + setAndLockPref("privacy.clearOnShutdown_v2.siteSettings", param); } else { let locked = true; // Needed to preserve original behavior in perpetuity. @@ -2042,12 +2058,22 @@ export var Policies = { param.Cache, locked ); + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown_v2.cache", + param.Cache, + locked + ); } else { PoliciesUtils.setDefaultPref( "privacy.clearOnShutdown.cache", false, lockDefaultPrefs ); + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown_v2.cache", + false, + lockDefaultPrefs + ); } if ("Cookies" in param) { PoliciesUtils.setDefaultPref( @@ -2055,12 +2081,26 @@ export var Policies = { param.Cookies, locked ); + + // We set cookiesAndStorage to follow lock and pref + // settings for cookies, and deprecate offlineApps + // and sessions in the new clear on shutdown dialog - Bug 1853996 + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown_v2.cookiesAndStorage", + param.Cookies, + locked + ); } else { PoliciesUtils.setDefaultPref( "privacy.clearOnShutdown.cookies", false, lockDefaultPrefs ); + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown_v2.cookiesAndStorage", + false, + lockDefaultPrefs + ); } if ("Downloads" in param) { PoliciesUtils.setDefaultPref( @@ -2094,12 +2134,26 @@ export var Policies = { param.History, locked ); + + // We set historyFormDataAndDownloads to follow lock and pref + // settings for history, and deprecate formdata and downloads + // in the new clear on shutdown dialog - Bug 1853996 + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads", + param.History, + locked + ); } else { PoliciesUtils.setDefaultPref( "privacy.clearOnShutdown.history", false, lockDefaultPrefs ); + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads", + false, + lockDefaultPrefs + ); } if ("Sessions" in param) { PoliciesUtils.setDefaultPref( @@ -2120,6 +2174,11 @@ export var Policies = { param.SiteSettings, locked ); + PoliciesUtils.setDefaultPref( + "privacy.clearOnShutdown_v2.siteSettings", + param.SiteSettings, + locked + ); } if ("OfflineApps" in param) { PoliciesUtils.setDefaultPref( @@ -2390,6 +2449,12 @@ export var Policies = { }, }, + TranslateEnabled: { + onBeforeAddons(manager, param) { + setAndLockPref("browser.translations.enable", param); + }, + }, + UserMessaging: { onBeforeAddons(manager, param) { if ("WhatsNew" in param) { @@ -2790,7 +2855,7 @@ function clearBlockedAboutPages() { gBlockedAboutPages = []; } -function blockAboutPage(manager, feature, neededOnContentProcess = false) { +function blockAboutPage(manager, feature) { addChromeURLBlocker(); gBlockedAboutPages.push(feature); @@ -2826,7 +2891,7 @@ let ChromeURLBlockPolicy = { } return Ci.nsIContentPolicy.ACCEPT; }, - shouldProcess(contentLocation, loadInfo) { + shouldProcess() { return Ci.nsIContentPolicy.ACCEPT; }, classDescription: "Policy Engine Content Policy", diff --git a/browser/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs b/browser/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs index 393b9bb85e..80968956ac 100644 --- a/browser/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs +++ b/browser/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs @@ -29,6 +29,22 @@ export var PROXY_TYPES_MAP = new Map([ ["autoConfig", Ci.nsIProtocolProxyService.PROXYCONFIG_PAC], ]); +let proxyPreferences = [ + "network.proxy.type", + "network.proxy.autoconfig_url", + "network.proxy.socks_remote_dns", + "signon.autologin.proxy", + "network.proxy.socks_version", + "network.proxy.no_proxies_on", + "network.proxy.share_proxy_settings", + "network.proxy.http", + "network.proxy.http_port", + "network.proxy.ssl", + "network.proxy.ssl_port", + "network.proxy.socks", + "network.proxy.socks_port", +]; + export var ProxyPolicies = { configureProxySettings(param, setPref) { if (param.Mode) { @@ -105,5 +121,13 @@ export var ProxyPolicies = { if (param.SOCKSProxy) { setProxyHostAndPort("socks", param.SOCKSProxy); } + + // All preferences should be locked regardless of whether or not a + // specific value was set. + if (param.Locked) { + for (let preference of proxyPreferences) { + Services.prefs.lockPref(preference); + } + } }, }; diff --git a/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs b/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs index 81f7955f27..26bae7acd9 100644 --- a/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs +++ b/browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs @@ -130,10 +130,10 @@ export let WebsiteFilter = { } return Ci.nsIContentPolicy.ACCEPT; }, - shouldProcess(contentLocation, loadInfo) { + shouldProcess() { return Ci.nsIContentPolicy.ACCEPT; }, - observe(subject, topic, data) { + observe(subject) { try { let channel = subject.QueryInterface(Ci.nsIHttpChannel); if ( diff --git a/browser/components/enterprisepolicies/schemas/policies-schema.json b/browser/components/enterprisepolicies/schemas/policies-schema.json index a1ccaed74f..3c578f2c4b 100644 --- a/browser/components/enterprisepolicies/schemas/policies-schema.json +++ b/browser/components/enterprisepolicies/schemas/policies-schema.json @@ -253,6 +253,12 @@ "DenyUrlRegexList": { "type": "string" }, + "AgentName": { + "type": "string" + }, + "ClientSignature": { + "type": "string" + }, "IsPerUser": { "type": "boolean" }, @@ -662,6 +668,9 @@ "items": { "type": "string" } + }, + "temporarily_allow_weak_signatures": { + "type": "boolean" } } } @@ -691,6 +700,9 @@ "default_area": { "type": "string", "enum": ["navbar", "menupanel"] + }, + "temporarily_allow_weak_signatures": { + "type": "boolean" } } } @@ -1422,6 +1434,10 @@ "required": ["Title", "URL"] }, + "TranslateEnabled": { + "type": "boolean" + }, + "UserMessaging": { "type": "object", "properties": { diff --git a/browser/components/enterprisepolicies/tests/browser/browser.toml b/browser/components/enterprisepolicies/tests/browser/browser.toml index 25ac681e5b..0517bb6557 100644 --- a/browser/components/enterprisepolicies/tests/browser/browser.toml +++ b/browser/components/enterprisepolicies/tests/browser/browser.toml @@ -117,6 +117,8 @@ https_first_disabled = true ["browser_policy_support_menu.js"] +["browser_policy_translateenabled.js"] + ["browser_policy_usermessaging.js"] ["browser_policy_websitefilter.js"] diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_pageinfo_permissions.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_pageinfo_permissions.js index 4921464782..2fc7892c8f 100644 --- a/browser/components/enterprisepolicies/tests/browser/browser_policy_pageinfo_permissions.js +++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_pageinfo_permissions.js @@ -58,8 +58,8 @@ add_task(async function test_pageinfo_permissions() { "xr", ]; - await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function (browser) { - let pageInfo = BrowserPageInfo(TEST_ORIGIN, "permTab"); + await BrowserTestUtils.withNewTab(TEST_ORIGIN, async function () { + let pageInfo = BrowserCommands.pageInfo(TEST_ORIGIN, "permTab"); await BrowserTestUtils.waitForEvent(pageInfo, "load"); for (let i = 0; i < permissions.length; i++) { diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_translateenabled.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_translateenabled.js new file mode 100644 index 0000000000..3658cf6388 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_translateenabled.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function setup() { + await setupPolicyEngineWithJson({ + policies: { + TranslateEnabled: false, + }, + }); +}); + +add_task(async function test_translate_pref_disabled() { + is( + Services.prefs.getBoolPref("browser.translations.enable"), + false, + "The translations pref should be disabled when the enterprise policy is active." + ); +}); + +add_task(async function test_translate_button_disabled() { + // Since testing will apply the policy after the browser has already started, + // we will need to open a new window to actually see changes from the policy + let win = await BrowserTestUtils.openNewBrowserWindow(); + + let appMenuButton = win.document.getElementById("PanelUI-menu-button"); + let viewShown = BrowserTestUtils.waitForEvent( + win.PanelUI.mainView, + "ViewShown" + ); + + appMenuButton.click(); + await viewShown; + + let translateSiteButton = win.document.getElementById( + "appMenu-translate-button" + ); + + is( + translateSiteButton.hidden, + true, + "The app-menu translate button should be hidden when the enterprise policy is active." + ); + + is( + translateSiteButton.disabled, + true, + "The app-menu translate button should be disabled when the enterprise policy is active." + ); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js b/browser/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js index 2f68436882..929f0470da 100644 --- a/browser/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js +++ b/browser/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js @@ -96,7 +96,7 @@ function waitForAboutDialog() { var domwindow = aXULWindow.docShell.domWindow; domwindow.addEventListener("load", aboutDialogOnLoad, true); }, - onCloseWindow: aXULWindow => {}, + onCloseWindow: () => {}, }; Services.wm.addListener(listener); diff --git a/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js b/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js index 27794aabbb..585218a016 100644 --- a/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js +++ b/browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js @@ -73,7 +73,7 @@ async function testPageBlockedByPolicy(page, policyJSON) { async browser => { BrowserTestUtils.startLoadingURIString(browser, page); await BrowserTestUtils.browserLoaded(browser, false, page, true); - await SpecialPowers.spawn(browser, [page], async function (innerPage) { + await SpecialPowers.spawn(browser, [page], async function () { ok( content.document.documentURI.startsWith( "about:neterror?e=blockedByPolicy" diff --git a/browser/components/enterprisepolicies/tests/browser/head.js b/browser/components/enterprisepolicies/tests/browser/head.js index bb08173aa9..dfa01fad0e 100644 --- a/browser/components/enterprisepolicies/tests/browser/head.js +++ b/browser/components/enterprisepolicies/tests/browser/head.js @@ -237,7 +237,7 @@ async function testPageBlockedByPolicy(page, policyJSON) { async browser => { BrowserTestUtils.startLoadingURIString(browser, page); await BrowserTestUtils.browserLoaded(browser, false, page, true); - await SpecialPowers.spawn(browser, [page], async function (innerPage) { + await SpecialPowers.spawn(browser, [page], async function () { ok( content.document.documentURI.startsWith( "about:neterror?e=blockedByPolicy" diff --git a/browser/components/enterprisepolicies/tests/xpcshell/head.js b/browser/components/enterprisepolicies/tests/xpcshell/head.js index 8b81261538..3881760ed4 100644 --- a/browser/components/enterprisepolicies/tests/xpcshell/head.js +++ b/browser/components/enterprisepolicies/tests/xpcshell/head.js @@ -116,7 +116,7 @@ function checkUserPref(prefName, prefValue) { ); } -function checkClearPref(prefName, prefValue) { +function checkClearPref(prefName) { equal( Services.prefs.prefHasUserValue(prefName), false, diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js index ee329a65f8..22a6269cce 100644 --- a/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js +++ b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js @@ -21,7 +21,7 @@ let themeID = "policytheme@mozilla.com"; let fileURL; -add_task(async function setup() { +add_setup(async function setup() { await AddonTestUtils.promiseStartupManager(); let webExtensionFile = AddonTestUtils.createTempWebExtensionFile({ @@ -34,6 +34,10 @@ add_task(async function setup() { }, }); + server.registerFile( + "/data/amosigned-sha1only.xpi", + do_get_file("amosigned-sha1only.xpi") + ); server.registerFile("/data/policy_test.xpi", webExtensionFile); fileURL = Services.io .newFileURI(webExtensionFile) @@ -289,3 +293,112 @@ add_task(async function test_addon_normalinstalled_file() { await addon.uninstall(); }); + +add_task(async function test_allow_weak_signatures() { + // Make sure weak signatures are restricted. + const resetWeakSignaturePref = + AddonTestUtils.setWeakSignatureInstallAllowed(false); + + const id = "amosigned-xpi@tests.mozilla.org"; + const perAddonSettings = { + installation_mode: "normal_installed", + install_url: BASE_URL + "/amosigned-sha1only.xpi", + }; + + info( + "Sanity check: expect install to fail if not allowed through enterprise policy settings" + ); + await Promise.all([ + AddonTestUtils.promiseInstallEvent("onDownloadFailed"), + setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + [id]: { ...perAddonSettings }, + }, + }, + }), + ]); + let addon = await AddonManager.getAddonByID(id); + equal(addon, null, "Add-on not installed"); + + info( + "Expect install to be allowed through per-addon enterprise policy settings" + ); + await Promise.all([ + AddonTestUtils.promiseInstallEvent("onInstallEnded"), + setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + [id]: { + ...perAddonSettings, + temporarily_allow_weak_signatures: true, + }, + }, + }, + }), + ]); + addon = await AddonManager.getAddonByID(id); + notEqual(addon, null, "Add-on not installed"); + await addon.uninstall(); + + info( + "Expect install to be allowed through global enterprise policy settings" + ); + await Promise.all([ + AddonTestUtils.promiseInstallEvent("onInstallEnded"), + setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + "*": { temporarily_allow_weak_signatures: true }, + [id]: { ...perAddonSettings }, + }, + }, + }), + ]); + addon = await AddonManager.getAddonByID(id); + notEqual(addon, null, "Add-on installed"); + await addon.uninstall(); + + info( + "Expect install to fail if allowed globally but disallowed by per-addon settings" + ); + await Promise.all([ + AddonTestUtils.promiseInstallEvent("onDownloadFailed"), + setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + "*": { temporarily_allow_weak_signatures: true }, + [id]: { + ...perAddonSettings, + temporarily_allow_weak_signatures: false, + }, + }, + }, + }), + ]); + addon = await AddonManager.getAddonByID(id); + equal(addon, null, "Add-on not installed"); + + info( + "Expect install to be allowed through per addon setting when globally disallowed" + ); + await Promise.all([ + AddonTestUtils.promiseInstallEvent("onInstallEnded"), + setupPolicyEngineWithJson({ + policies: { + ExtensionSettings: { + "*": { temporarily_allow_weak_signatures: false }, + [id]: { + ...perAddonSettings, + temporarily_allow_weak_signatures: true, + }, + }, + }, + }), + ]); + addon = await AddonManager.getAddonByID(id); + notEqual(addon, null, "Add-on installed"); + await addon.uninstall(); + + resetWeakSignaturePref(); +}); diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js b/browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js index 5908b2d35c..bfed2491be 100644 --- a/browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js +++ b/browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js @@ -12,7 +12,7 @@ const REQ_LOC_CHANGE_EVENT = "intl:requested-locales-changed"; function promiseLocaleChanged(requestedLocale) { return new Promise(resolve => { let localeObserver = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case REQ_LOC_CHANGE_EVENT: let reqLocs = Services.locale.requestedLocales; @@ -26,10 +26,10 @@ function promiseLocaleChanged(requestedLocale) { }); } -function promiseLocaleNotChanged(requestedLocale) { +function promiseLocaleNotChanged() { return new Promise(resolve => { let localeObserver = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case REQ_LOC_CHANGE_EVENT: ok(false, "Locale should not change."); diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js b/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js index 82caee16a7..c0952d0627 100644 --- a/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js +++ b/browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js @@ -227,6 +227,10 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.sessions": true, "privacy.clearOnShutdown.siteSettings": true, "privacy.clearOnShutdown.offlineApps": true, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": true, + "privacy.clearOnShutdown_v2.cookiesAndStorage": true, + "privacy.clearOnShutdown_v2.cache": true, + "privacy.clearOnShutdown_v2.siteSettings": true, }, }, @@ -244,6 +248,10 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.sessions": false, "privacy.clearOnShutdown.siteSettings": false, "privacy.clearOnShutdown.offlineApps": false, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, + "privacy.clearOnShutdown_v2.cache": false, + "privacy.clearOnShutdown_v2.siteSettings": false, }, }, @@ -261,6 +269,9 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.formdata": false, "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": false, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, + "privacy.clearOnShutdown_v2.cache": true, }, }, @@ -278,6 +289,9 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.formdata": false, "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": false, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": true, + "privacy.clearOnShutdown_v2.cache": false, }, }, @@ -295,6 +309,9 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.formdata": false, "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": false, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, + "privacy.clearOnShutdown_v2.cache": false, }, }, @@ -312,6 +329,9 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.formdata": true, "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": false, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, + "privacy.clearOnShutdown_v2.cache": false, }, }, @@ -329,6 +349,9 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.formdata": false, "privacy.clearOnShutdown.history": true, "privacy.clearOnShutdown.sessions": false, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": true, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, + "privacy.clearOnShutdown_v2.cache": false, }, }, @@ -346,6 +369,9 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.formdata": false, "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": true, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, + "privacy.clearOnShutdown_v2.cache": false, }, }, @@ -364,6 +390,10 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": false, "privacy.clearOnShutdown.siteSettings": true, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, + "privacy.clearOnShutdown_v2.cache": false, + "privacy.clearOnShutdown_v2.siteSettings": true, }, }, @@ -382,6 +412,9 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": false, "privacy.clearOnShutdown.offlineApps": true, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, + "privacy.clearOnShutdown_v2.cache": false, }, }, @@ -396,6 +429,7 @@ const POLICIES_TESTS = [ lockedPrefs: { "privacy.sanitize.sanitizeOnShutdown": true, "privacy.clearOnShutdown.cache": true, + "privacy.clearOnShutdown_v2.cache": true, }, unlockedPrefs: { "privacy.clearOnShutdown.cookies": false, @@ -403,6 +437,8 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.formdata": false, "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": false, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, }, }, @@ -418,12 +454,15 @@ const POLICIES_TESTS = [ "privacy.sanitize.sanitizeOnShutdown": true, "privacy.clearOnShutdown.cache": true, "privacy.clearOnShutdown.cookies": false, + "privacy.clearOnShutdown_v2.cache": true, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, }, unlockedPrefs: { "privacy.clearOnShutdown.downloads": false, "privacy.clearOnShutdown.formdata": false, "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": false, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, }, }, @@ -442,6 +481,9 @@ const POLICIES_TESTS = [ "privacy.clearOnShutdown.formdata": false, "privacy.clearOnShutdown.history": false, "privacy.clearOnShutdown.sessions": false, + "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": false, + "privacy.clearOnShutdown_v2.cookiesAndStorage": false, + "privacy.clearOnShutdown_v2.cache": true, }, }, @@ -1045,6 +1087,18 @@ const POLICIES_TESTS = [ "extensions.formautofill.creditCards.enabled": false, }, }, + + // POLICY: Proxy - locking if no values are set + { + policies: { + Proxy: { + Locked: true, + }, + }, + lockedPrefs: { + "network.proxy.type": 5, + }, + }, ]; add_task(async function test_policy_simple_prefs() { diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js b/browser/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js index 0d246c850c..73755b1dbc 100644 --- a/browser/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js +++ b/browser/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js @@ -32,7 +32,7 @@ add_task(async function test_policies_sorted() { ); checkArrayIsSorted( Object.keys(Policies), - "Policies.jsm is alphabetically sorted." + "Policies.sys.mjs is alphabetically sorted." ); }); diff --git a/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.toml b/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.toml index 69dd3e5103..b21e0f9022 100644 --- a/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.toml +++ b/browser/components/enterprisepolicies/tests/xpcshell/xpcshell.toml @@ -2,7 +2,10 @@ skip-if = ["os == 'android'"] # bug 1730213 firefox-appdir = "browser" head = "head.js" -support-files = ["policytest_v0.1.xpi"] +support-files = [ + "policytest_v0.1.xpi", + "../../../../../toolkit/mozapps/extensions/test/xpinstall/amosigned-sha1only.xpi" +] ["test_3rdparty.js"] diff --git a/browser/components/extensions/parent/ext-browser.js b/browser/components/extensions/parent/ext-browser.js index 7b01d15101..d2f72d4f46 100644 --- a/browser/components/extensions/parent/ext-browser.js +++ b/browser/components/extensions/parent/ext-browser.js @@ -67,8 +67,12 @@ global.openOptionsPage = extension => { return Promise.reject({ message: "No browser window available" }); } - if (extension.manifest.options_ui.open_in_tab) { - window.switchToTabHavingURI(extension.manifest.options_ui.page, true, { + const { optionsPageProperties } = extension; + if (!optionsPageProperties) { + return Promise.reject({ message: "No options page" }); + } + if (optionsPageProperties.open_in_tab) { + window.switchToTabHavingURI(optionsPageProperties.page, true, { triggeringPrincipal: extension.principal, }); return Promise.resolve(); diff --git a/browser/components/extensions/parent/ext-chrome-settings-overrides.js b/browser/components/extensions/parent/ext-chrome-settings-overrides.js index 1fbb794b51..3d1b7d363e 100644 --- a/browser/components/extensions/parent/ext-chrome-settings-overrides.js +++ b/browser/components/extensions/parent/ext-chrome-settings-overrides.js @@ -56,7 +56,7 @@ ChromeUtils.defineLazyGetter(this, "homepagePopup", () => { Services.prefs.addObserver(HOMEPAGE_PREF, async function prefObserver() { Services.prefs.removeObserver(HOMEPAGE_PREF, prefObserver); let loaded = waitForTabLoaded(tab); - win.BrowserHome(); + win.BrowserCommands.home(); await loaded; // Manually trigger an event in case this is controlled again. popup.open(); diff --git a/browser/components/extensions/parent/ext-commands.js b/browser/components/extensions/parent/ext-commands.js index 328f05a802..5b2b5f11b2 100644 --- a/browser/components/extensions/parent/ext-commands.js +++ b/browser/components/extensions/parent/ext-commands.js @@ -13,8 +13,13 @@ ChromeUtils.defineESModuleGetters(this, { this.commands = class extends ExtensionAPIPersistent { PERSISTENT_EVENTS = { onCommand({ fire }) { + const { extension } = this; + const { tabManager } = extension; + let listener = (eventName, commandName) => { - fire.async(commandName); + let nativeTab = tabTracker.activeTab; + tabManager.addActiveTabPermission(nativeTab); + fire.async(commandName, tabManager.convert(nativeTab)); }; this.on("command", listener); return { diff --git a/browser/components/extensions/parent/ext-devtools-panels.js b/browser/components/extensions/parent/ext-devtools-panels.js index 6b83ea5dbb..4b88b91eab 100644 --- a/browser/components/extensions/parent/ext-devtools-panels.js +++ b/browser/components/extensions/parent/ext-devtools-panels.js @@ -104,20 +104,21 @@ class BaseDevToolsPanel { /** * Represents an addon devtools panel in the main process. - * - * @param {ExtensionChildProxyContext} context - * A devtools extension proxy context running in a main process. - * @param {object} options - * @param {string} options.id - * The id of the addon devtools panel. - * @param {string} options.icon - * The icon of the addon devtools panel. - * @param {string} options.title - * The title of the addon devtools panel. - * @param {string} options.url - * The url of the addon devtools panel, relative to the extension base URL. */ class ParentDevToolsPanel extends BaseDevToolsPanel { + /** + * @param {DevToolsExtensionPageContextParent} context + * A devtools extension proxy context running in a main process. + * @param {object} panelOptions + * @param {string} panelOptions.id + * The id of the addon devtools panel. + * @param {string} panelOptions.icon + * The icon of the addon devtools panel. + * @param {string} panelOptions.title + * The title of the addon devtools panel. + * @param {string} panelOptions.url + * The url of the addon devtools panel, relative to the extension base URL. + */ constructor(context, panelOptions) { super(context, panelOptions); @@ -339,16 +340,17 @@ class DevToolsSelectionObserver extends EventEmitter { /** * Represents an addon devtools inspector sidebar in the main process. - * - * @param {ExtensionChildProxyContext} context - * A devtools extension proxy context running in a main process. - * @param {object} options - * @param {string} options.id - * The id of the addon devtools sidebar. - * @param {string} options.title - * The title of the addon devtools sidebar. */ class ParentDevToolsInspectorSidebar extends BaseDevToolsPanel { + /** + * @param {DevToolsExtensionPageContextParent} context + * A devtools extension proxy context running in a main process. + * @param {object} panelOptions + * @param {string} panelOptions.id + * The id of the addon devtools sidebar. + * @param {string} panelOptions.title + * The title of the addon devtools sidebar. + */ constructor(context, panelOptions) { super(context, panelOptions); diff --git a/browser/components/extensions/parent/ext-tabs.js b/browser/components/extensions/parent/ext-tabs.js index 128a42439b..4b8d296d67 100644 --- a/browser/components/extensions/parent/ext-tabs.js +++ b/browser/components/extensions/parent/ext-tabs.js @@ -1026,7 +1026,13 @@ this.tabs = class extends ExtensionAPIPersistent { ? windowTracker.getTopWindow(context) : windowTracker.getWindow(windowId, context); - let tab = tabManager.wrapTab(window.gBrowser.selectedTab); + let tab = tabManager.getWrapper(window.gBrowser.selectedTab); + if ( + !extension.hasPermission("<all_urls>") && + !tab.hasActiveTabPermission + ) { + throw new ExtensionError("Missing activeTab permission"); + } await tabListener.awaitTabReady(tab.nativeTab); let zoom = window.ZoomManager.getZoomForBrowser( diff --git a/browser/components/extensions/schemas/commands.json b/browser/components/extensions/schemas/commands.json index 19e8e122f9..30942d0aab 100644 --- a/browser/components/extensions/schemas/commands.json +++ b/browser/components/extensions/schemas/commands.json @@ -104,6 +104,12 @@ { "name": "command", "type": "string" + }, + { + "name": "tab", + "$ref": "tags.Tab", + "optional": true, + "description": "Details of the $(ref:tabs.Tab) where the command was activated." } ] }, diff --git a/browser/components/extensions/schemas/tabs.json b/browser/components/extensions/schemas/tabs.json index ee7cd3dd93..55fccee0b5 100644 --- a/browser/components/extensions/schemas/tabs.json +++ b/browser/components/extensions/schemas/tabs.json @@ -1198,8 +1198,8 @@ { "name": "captureVisibleTab", "type": "function", - "description": "Captures an area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[<all_urls>] permission to use this method.", - "permissions": ["<all_urls>"], + "description": "Captures an area of the currently active tab in the specified window. You must have <all_urls> or activeTab permission to use this method.", + "permissions": ["<all_urls>", "activeTab"], "async": "callback", "parameters": [ { diff --git a/browser/components/extensions/test/browser/browser.toml b/browser/components/extensions/test/browser/browser.toml index 417bad7e31..570a51eeb4 100644 --- a/browser/components/extensions/test/browser/browser.toml +++ b/browser/components/extensions/test/browser/browser.toml @@ -46,10 +46,13 @@ support-files = [ "empty.xpi", "../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js", "../../../../../toolkit/components/extensions/test/mochitest/redirection.sjs", - "../../../../../toolkit/components/reader/test/readerModeNonArticle.html", - "../../../../../toolkit/components/reader/test/readerModeArticle.html", + "../../../../../toolkit/components/reader/tests/browser/readerModeNonArticle.html", + "../../../../../toolkit/components/reader/tests/browser/readerModeArticle.html", +] +skip-if = [ + "os == 'linux' && os_version == '18.04' && asan", # Bug 1721945 - Software WebRender + "os == 'linux' && os_version == '18.04' && tsan", # manifest runs too long ] -skip-if = ["os == 'linux' && os_version == '18.04' && asan"] # Bug 1721945 - Software WebRender ["browser_AMBrowserExtensionsImport.js"] @@ -700,7 +703,6 @@ tags = "fullscreen" ["browser_toolbar_prefers_color_scheme.js"] ["browser_unified_extensions.js"] -fail-if = ["a11y_checks"] # Bug 1854460 clicked browser may not be accessible ["browser_unified_extensions_accessibility.js"] diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js index e5d315c5d2..a55952e610 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js @@ -184,7 +184,11 @@ async function runTests(options) { is(getListStyleImage(button), details.icon, "icon URL is correct"); is(button.getAttribute("tooltiptext"), title, "image title is correct"); is(button.getAttribute("label"), title, "image label is correct"); - is(button.getAttribute("badge"), details.badge, "badge text is correct"); + is( + button.getAttribute("badge") || "", + details.badge, + "badge text is correct" + ); is( button.getAttribute("disabled") == "true", !details.enabled, diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js index 8e89457904..26d1536de1 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js @@ -430,9 +430,6 @@ async function browseraction_contextmenu_remove_extension_helper() { }, useAddonManager: "temporary", }); - let brand = Services.strings - .createBundle("chrome://branding/locale/brand.properties") - .GetStringFromName("brandShorterName"); let { prompt } = Services; let promptService = { _response: 1, @@ -466,9 +463,6 @@ async function browseraction_contextmenu_remove_extension_helper() { await closeChromeContextMenu(menuId, removeExtension); let args = await confirmArgs; is(args[1], `Remove ${name}?`); - if (!Services.prefs.getBoolPref("prompts.windowPromptSubDialog", false)) { - is(args[2], `Remove ${name} from ${brand}?`); - } is(args[4], "Remove"); return menu; } diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js index 98e66e6c7a..289cbf8a88 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js @@ -50,7 +50,7 @@ async function installTestAddon(addonId, unpacked = false) { // This temporary directory is going to be removed from the // cleanup function, but also make it unique as we do for the // other temporary files (e.g. like getTemporaryFile as defined - // in XPInstall.jsm). + // in XPIInstall.sys.mjs). const random = Math.round(Math.random() * 36 ** 3).toString(36); const tmpDirName = `mochitest_unpacked_addons_${random}`; let tmpExtPath = FileUtils.getDir("TmpD", [tmpDirName]); diff --git a/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js b/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js index b67952c03c..1c9ee9a6c1 100644 --- a/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js +++ b/browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js @@ -388,7 +388,7 @@ add_task(async function test_doorhanger_homepage_button() { await ext2.startup(); let popupShown = promisePopupShown(panel); - BrowserHome(); + BrowserCommands.home(); await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, () => gURLBar.value.endsWith("ext2.html") ); @@ -410,7 +410,7 @@ add_task(async function test_doorhanger_homepage_button() { popupShown = promisePopupShown(panel); await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); let openHomepage = TestUtils.topicObserved("browser-open-homepage-start"); - BrowserHome(); + BrowserCommands.home(); await openHomepage; await popupShown; await TestUtils.waitForCondition( @@ -432,7 +432,7 @@ add_task(async function test_doorhanger_homepage_button() { BrowserTestUtils.removeTab(gBrowser.selectedTab); openHomepage = TestUtils.topicObserved("browser-open-homepage-start"); - BrowserHome(); + BrowserCommands.home(); await openHomepage; is(getHomePageURL(), defaultHomePage, "The homepage is set back to default"); @@ -507,7 +507,7 @@ add_task(async function test_doorhanger_new_window() { let popupShown = promisePopupShown(panel); await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank"); let openHomepage = TestUtils.topicObserved("browser-open-homepage-start"); - win.BrowserHome(); + win.BrowserCommands.home(); await openHomepage; await popupShown; @@ -547,7 +547,7 @@ async function testHomePageWindow(options = {}) { let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(doc); let popupShown = options.expectPanel && promisePopupShown(panel); - win.BrowserHome(); + win.BrowserCommands.home(); await Promise.all([ BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser), openHomepage, diff --git a/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js index db900f7ea4..abde8f90f7 100644 --- a/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js +++ b/browser/components/extensions/test/browser/browser_ext_commands_onCommand.js @@ -226,7 +226,16 @@ add_task(async function test_user_defined_commands() { } function background() { - browser.commands.onCommand.addListener(commandName => { + browser.commands.onCommand.addListener(async (commandName, tab) => { + let [expectedTab] = await browser.tabs.query({ + currentWindow: true, + active: true, + }); + browser.test.assertEq( + tab.id, + expectedTab.id, + "Expected onCommand listener to pass the current tab" + ); browser.test.sendMessage("oncommand", commandName); }); browser.test.sendMessage("ready"); @@ -408,8 +417,9 @@ add_task(async function test_commands_event_page() { }, }, background() { - browser.commands.onCommand.addListener(name => { + browser.commands.onCommand.addListener((name, tab) => { browser.test.assertEq(name, "toggle-feature", "command received"); + browser.test.assertTrue(!!tab, "tab received"); browser.test.sendMessage("onCommand"); }); browser.test.sendMessage("ready"); diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js b/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js index 548c35399f..33ad85f268 100644 --- a/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js +++ b/browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js @@ -8,11 +8,17 @@ function extensionScript() { let FRAME_URL = browser.runtime.getManifest().content_scripts[0].matches[0]; // Cannot use :8888 in the manifest because of bug 1468162. FRAME_URL = FRAME_URL.replace("mochi.test", "mochi.test:8888"); + let FRAME_ORIGIN = new URL(FRAME_URL).origin; browser.runtime.onConnect.addListener(port => { browser.test.assertEq(port.sender.tab, undefined, "Sender is not a tab"); browser.test.assertEq(port.sender.frameId, undefined, "frameId unset"); browser.test.assertEq(port.sender.url, FRAME_URL, "Expected sender URL"); + browser.test.assertEq( + port.sender.origin, + FRAME_ORIGIN, + "Expected sender origin" + ); port.onMessage.addListener(msg => { browser.test.assertEq("pong", msg, "Reply from content script"); diff --git a/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js b/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js index f751c3c202..08f19013c4 100644 --- a/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js +++ b/browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js @@ -2,12 +2,16 @@ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; +const FILE_URL = Services.io.newFileURI( + new FileUtils.File(getTestFilePath("file_dummy.html")) +).spec; + add_task(async function test_sender_url() { let extension = ExtensionTestUtils.loadExtension({ manifest: { content_scripts: [ { - matches: ["http://mochi.test/*"], + matches: ["http://mochi.test/*", "file:///*"], run_at: "document_start", js: ["script.js"], }, @@ -18,6 +22,7 @@ add_task(async function test_sender_url() { browser.runtime.onMessage.addListener((msg, sender) => { browser.test.log("Message received."); browser.test.sendMessage("sender.url", sender.url); + browser.test.sendMessage("sender.origin", sender.origin); }); }, @@ -53,6 +58,9 @@ add_task(async function test_sender_url() { let url = await extension.awaitMessage("sender.url"); is(url, image, `Correct sender.url: ${url}`); + let origin = await extension.awaitMessage("sender.origin"); + is(origin, "http://mochi.test:8888", `Correct sender.origin: ${origin}`); + let wentBack = awaitNewTab(); await browser.goBack(); await wentBack; @@ -60,6 +68,17 @@ add_task(async function test_sender_url() { await browser.goForward(); url = await extension.awaitMessage("sender.url"); is(url, image, `Correct sender.url: ${url}`); + + origin = await extension.awaitMessage("sender.origin"); + is(origin, "http://mochi.test:8888", `Correct sender.origin: ${origin}`); + }); + + await BrowserTestUtils.withNewTab(FILE_URL, async () => { + let url = await extension.awaitMessage("sender.url"); + ok(url.endsWith("/file_dummy.html"), `Correct sender.url: ${url}`); + + let origin = await extension.awaitMessage("sender.origin"); + is(origin, "null", `Correct sender.origin: ${origin}`); }); await extension.unload(); diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js index 30d4d528b2..cb81d9a93b 100644 --- a/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js @@ -269,7 +269,7 @@ add_task(async function test_manifest_without_icons() { let items = menu.getElementsByAttribute("label", "first item"); is(items.length, 1, "Found first item"); // manifest.json does not declare icons, so the root menu item shouldn't have an icon either. - is(items[0].getAttribute("image"), "", "Root menu must not have an icon"); + is(items[0].getAttribute("image"), null, "Root menu must not have an icon"); await closeExtensionContextMenu(items[0]); await extension.awaitMessage("added-second-item"); @@ -281,7 +281,7 @@ add_task(async function test_manifest_without_icons() { is(items.length, 1, "Auto-generated root item exists"); is( items[0].getAttribute("image"), - "", + null, "Auto-generated menu root must not have an icon" ); @@ -464,7 +464,7 @@ add_task(async function test_child_icon_update() { contextMenuChild2 = contextMenu.getElementsByAttribute("label", "child2")[0]; is( contextMenuChild2.getAttribute("image"), - "", + null, "Second child should not have an icon" ); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js b/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js index 0004f60853..014b6dddf2 100644 --- a/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js +++ b/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js @@ -26,7 +26,7 @@ add_task(async function test_onCreated_active() { await extension.startup(); await extension.awaitMessage("ready"); - BrowserOpenTab(); + BrowserCommands.openTab(); let tab = await extension.awaitMessage("onCreated"); is(true, tab.active, "Tab should be active"); diff --git a/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js index 988f44bd5d..a3ccc5a521 100644 --- a/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js +++ b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js @@ -45,7 +45,7 @@ async function promiseNewTab(expectUrl = AboutNewTab.newTabURL, win = window) { `Should open correct new tab url ${expectUrl}.` ); - win.BrowserOpenTab(); + win.BrowserCommands.openTab(); const newTabCreatedPromise = newTabStartPromise; const browser = await newTabCreatedPromise; await newtabShown; diff --git a/browser/components/extensions/test/browser/browser_unified_extensions.js b/browser/components/extensions/test/browser/browser_unified_extensions.js index 7ab7753c0e..2c65f47c4e 100644 --- a/browser/components/extensions/test/browser/browser_unified_extensions.js +++ b/browser/components/extensions/test/browser/browser_unified_extensions.js @@ -1222,7 +1222,11 @@ add_task(async function test_hover_message_when_button_updates_itself() { // Move cursor to the center of the entire browser UI to avoid issues with // other focus/hover checks. We do this to avoid intermittent test failures. + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive content of the page. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); EventUtils.synthesizeMouseAtCenter(document.documentElement, {}); + AccessibilityUtils.resetEnv(); await extension.unload(); }); diff --git a/browser/components/firefoxview/HistoryController.mjs b/browser/components/firefoxview/HistoryController.mjs new file mode 100644 index 0000000000..d2bda5cec2 --- /dev/null +++ b/browser/components/firefoxview/HistoryController.mjs @@ -0,0 +1,188 @@ +/* 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FirefoxViewPlacesQuery: + "resource:///modules/firefox-view-places-query.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +let XPCOMUtils = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +).XPCOMUtils; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "maxRowsPref", + "browser.firefox-view.max-history-rows", + -1 +); + +const HISTORY_MAP_L10N_IDS = { + sidebar: { + "history-date-today": "sidebar-history-date-today", + "history-date-yesterday": "sidebar-history-date-yesterday", + "history-date-this-month": "sidebar-history-date-this-month", + "history-date-prev-month": "sidebar-history-date-prev-month", + }, + firefoxview: { + "history-date-today": "firefoxview-history-date-today", + "history-date-yesterday": "firefoxview-history-date-yesterday", + "history-date-this-month": "firefoxview-history-date-this-month", + "history-date-prev-month": "firefoxview-history-date-prev-month", + }, +}; + +export class HistoryController { + host; + allHistoryItems; + historyMapByDate; + historyMapBySite; + searchQuery; + searchResults; + sortOption; + + constructor(host, options) { + this.allHistoryItems = new Map(); + this.historyMapByDate = []; + this.historyMapBySite = []; + this.placesQuery = new lazy.FirefoxViewPlacesQuery(); + this.searchQuery = ""; + this.searchResults = null; + this.sortOption = "date"; + this.searchResultsLimit = options?.searchResultsLimit || 300; + this.component = HISTORY_MAP_L10N_IDS?.[options?.component] + ? options?.component + : "firefoxview"; + this.host = host; + + host.addController(this); + } + + async hostConnected() { + this.placesQuery.observeHistory(data => this.updateAllHistoryItems(data)); + await this.updateHistoryData(); + this.createHistoryMaps(); + } + + hostDisconnected() { + this.placesQuery.close(); + } + + deleteFromHistory() { + lazy.PlacesUtils.history.remove(this.host.triggerNode.url); + } + + async onSearchQuery(e) { + this.searchQuery = e.detail.query; + await this.updateSearchResults(); + this.host.requestUpdate(); + } + + async onChangeSortOption(e) { + this.sortOption = e.target.value; + await this.updateHistoryData(); + await this.updateSearchResults(); + this.host.requestUpdate(); + } + + async updateHistoryData() { + this.allHistoryItems = await this.placesQuery.getHistory({ + daysOld: 60, + limit: lazy.maxRowsPref, + sortBy: this.sortOption, + }); + } + + async updateAllHistoryItems(allHistoryItems) { + if (allHistoryItems) { + this.allHistoryItems = allHistoryItems; + } else { + await this.updateHistoryData(); + } + this.resetHistoryMaps(); + this.host.requestUpdate(); + await this.updateSearchResults(); + } + + async updateSearchResults() { + if (this.searchQuery) { + try { + this.searchResults = await this.placesQuery.searchHistory( + this.searchQuery, + this.searchResultsLimit + ); + } catch (e) { + // Connection interrupted, ignore. + } + } else { + this.searchResults = null; + } + } + + resetHistoryMaps() { + this.historyMapByDate = []; + this.historyMapBySite = []; + } + + createHistoryMaps() { + if (!this.historyMapByDate.length) { + const { + visitsFromToday, + visitsFromYesterday, + visitsByDay, + visitsByMonth, + } = this.placesQuery; + + // Add visits from today and yesterday. + if (visitsFromToday.length) { + this.historyMapByDate.push({ + l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-today"], + items: visitsFromToday, + }); + } + if (visitsFromYesterday.length) { + this.historyMapByDate.push({ + l10nId: + HISTORY_MAP_L10N_IDS[this.component]["history-date-yesterday"], + items: visitsFromYesterday, + }); + } + + // Add visits from this month, grouped by day. + visitsByDay.forEach(visits => { + this.historyMapByDate.push({ + l10nId: + HISTORY_MAP_L10N_IDS[this.component]["history-date-this-month"], + items: visits, + }); + }); + + // Add visits from previous months, grouped by month. + visitsByMonth.forEach(visits => { + this.historyMapByDate.push({ + l10nId: + HISTORY_MAP_L10N_IDS[this.component]["history-date-prev-month"], + items: visits, + }); + }); + } else if ( + this.sortOption === "site" && + !this.historyMapBySite.length && + this.component === "firefoxview" + ) { + this.historyMapBySite = Array.from( + this.allHistoryItems.entries(), + ([domain, items]) => ({ + domain, + items, + l10nId: domain ? null : "firefoxview-history-site-localhost", + }) + ).sort((a, b) => a.domain.localeCompare(b.domain)); + } + this.host.requestUpdate(); + } +} diff --git a/browser/components/firefoxview/OpenTabs.sys.mjs b/browser/components/firefoxview/OpenTabs.sys.mjs index 0771bf9e65..6d67ca44cc 100644 --- a/browser/components/firefoxview/OpenTabs.sys.mjs +++ b/browser/components/firefoxview/OpenTabs.sys.mjs @@ -33,6 +33,7 @@ const TAB_CHANGE_EVENTS = Object.freeze([ ]); const TAB_RECENCY_CHANGE_EVENTS = Object.freeze([ "activate", + "sizemodechange", "TabAttrModified", "TabClose", "TabOpen", @@ -75,6 +76,10 @@ class OpenTabsTarget extends EventTarget { TabChange: new Set(), TabRecencyChange: new Set(), }; + #sourceEventsByType = { + TabChange: new Set(), + TabRecencyChange: new Set(), + }; #dispatchChangesTask; #started = false; #watchedWindows = new Set(); @@ -143,7 +148,7 @@ class OpenTabsTarget extends EventTarget { windowList.map(win => win.delayedStartupPromise) ).then(() => { // re-filter the list as properties might have changed in the interim - return windowList.filter(win => this.includeWindowFilter); + return windowList.filter(() => this.includeWindowFilter); }); } @@ -223,6 +228,9 @@ class OpenTabsTarget extends EventTarget { for (let changedWindows of Object.values(this.#changedWindowsByType)) { changedWindows.clear(); } + for (let sourceEvents of Object.values(this.#sourceEventsByType)) { + sourceEvents.clear(); + } this.#watchedWindows.clear(); this.#dispatchChangesTask?.disarm(); } @@ -245,9 +253,16 @@ class OpenTabsTarget extends EventTarget { tabContainer.addEventListener("TabUnpinned", this); tabContainer.addEventListener("TabSelect", this); win.addEventListener("activate", this); + win.addEventListener("sizemodechange", this); - this.#scheduleEventDispatch("TabChange", {}); - this.#scheduleEventDispatch("TabRecencyChange", {}); + this.#scheduleEventDispatch("TabChange", { + sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: "watchWindow", + }); + this.#scheduleEventDispatch("TabRecencyChange", { + sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: "watchWindow", + }); } /** @@ -270,9 +285,16 @@ class OpenTabsTarget extends EventTarget { tabContainer.removeEventListener("TabSelect", this); tabContainer.removeEventListener("TabUnpinned", this); win.removeEventListener("activate", this); + win.removeEventListener("sizemodechange", this); - this.#scheduleEventDispatch("TabChange", {}); - this.#scheduleEventDispatch("TabRecencyChange", {}); + this.#scheduleEventDispatch("TabChange", { + sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: "unwatchWindow", + }); + this.#scheduleEventDispatch("TabRecencyChange", { + sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: "unwatchWindow", + }); } } @@ -281,11 +303,12 @@ class OpenTabsTarget extends EventTarget { * Repeated calls within approx 16ms will be consolidated * into one event dispatch. */ - #scheduleEventDispatch(eventType, { sourceWindowId } = {}) { + #scheduleEventDispatch(eventType, { sourceWindowId, sourceEvent } = {}) { if (!this.haveListenersForEvent(eventType)) { return; } + this.#sourceEventsByType[eventType].add(sourceEvent); this.#changedWindowsByType[eventType].add(sourceWindowId); // Queue up an event dispatch - we use a deferred task to make this less noisy by // consolidating multiple change events into one. @@ -302,16 +325,18 @@ class OpenTabsTarget extends EventTarget { for (let [eventType, changedWindowIds] of Object.entries( this.#changedWindowsByType )) { + let sourceEvents = this.#sourceEventsByType[eventType]; if (this.haveListenersForEvent(eventType) && changedWindowIds.size) { - this.dispatchEvent( - new CustomEvent(eventType, { - detail: { - windowIds: [...changedWindowIds], - }, - }) - ); + let changeEvent = new CustomEvent(eventType, { + detail: { + windowIds: [...changedWindowIds], + sourceEvents: [...sourceEvents], + }, + }); + this.dispatchEvent(changeEvent); changedWindowIds.clear(); } + sourceEvents?.clear(); } } @@ -362,11 +387,13 @@ class OpenTabsTarget extends EventTarget { if (TAB_RECENCY_CHANGE_EVENTS.includes(type)) { this.#scheduleEventDispatch("TabRecencyChange", { sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: type, }); } if (TAB_CHANGE_EVENTS.includes(type)) { this.#scheduleEventDispatch("TabChange", { sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: type, }); } } @@ -377,7 +404,7 @@ const gExclusiveWindows = new (class { constructor() { Services.obs.addObserver(this, "domwindowclosed"); } - observe(subject, topic, data) { + observe(subject) { let win = subject; let winTarget = this.perWindowInstances.get(win); if (winTarget) { diff --git a/browser/components/firefoxview/SyncedTabsController.sys.mjs b/browser/components/firefoxview/SyncedTabsController.sys.mjs new file mode 100644 index 0000000000..6ab8249bfe --- /dev/null +++ b/browser/components/firefoxview/SyncedTabsController.sys.mjs @@ -0,0 +1,333 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", + SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", +}); + +import { SyncedTabsErrorHandler } from "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs"; +import { TabsSetupFlowManager } from "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"; +import { searchTabList } from "chrome://browser/content/firefoxview/helpers.mjs"; + +const SYNCED_TABS_CHANGED = "services.sync.tabs.changed"; +const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed"; + +/** + * The controller for synced tabs components. + * + * @implements {ReactiveController} + */ +export class SyncedTabsController { + /** + * @type {boolean} + */ + contextMenu; + currentSetupStateIndex = -1; + currentSyncedTabs = []; + devices = []; + /** + * The current error state as determined by `SyncedTabsErrorHandler`. + * + * @type {number} + */ + errorState = null; + /** + * Component associated with this controller. + * + * @type {ReactiveControllerHost} + */ + host; + /** + * @type {Function} + */ + pairDeviceCallback; + searchQuery = ""; + /** + * @type {Function} + */ + signupCallback; + + /** + * Construct a new SyncedTabsController. + * + * @param {ReactiveControllerHost} host + * @param {object} options + * @param {boolean} [options.contextMenu] + * Whether synced tab items have a secondary context menu. + * @param {Function} [options.pairDeviceCallback] + * The function to call when the pair device window is opened. + * @param {Function} [options.signupCallback] + * The function to call when the signup window is opened. + */ + constructor(host, { contextMenu, pairDeviceCallback, signupCallback } = {}) { + this.contextMenu = contextMenu; + this.pairDeviceCallback = pairDeviceCallback; + this.signupCallback = signupCallback; + this.observe = this.observe.bind(this); + this.host = host; + this.host.addController(this); + } + + hostConnected() { + this.host.addEventListener("click", this); + } + + hostDisconnected() { + this.host.removeEventListener("click", this); + } + + addSyncObservers() { + Services.obs.addObserver(this.observe, SYNCED_TABS_CHANGED); + Services.obs.addObserver(this.observe, TOPIC_SETUPSTATE_CHANGED); + } + + removeSyncObservers() { + Services.obs.removeObserver(this.observe, SYNCED_TABS_CHANGED); + Services.obs.removeObserver(this.observe, TOPIC_SETUPSTATE_CHANGED); + } + + handleEvent(event) { + if (event.type == "click" && event.target.dataset.action) { + const { ErrorType } = SyncedTabsErrorHandler; + switch (event.target.dataset.action) { + case `${ErrorType.SYNC_ERROR}`: + case `${ErrorType.NETWORK_OFFLINE}`: + case `${ErrorType.PASSWORD_LOCKED}`: { + TabsSetupFlowManager.tryToClearError(); + break; + } + case `${ErrorType.SIGNED_OUT}`: + case "sign-in": { + TabsSetupFlowManager.openFxASignup(event.target.ownerGlobal); + this.signupCallback?.(); + break; + } + case "add-device": { + TabsSetupFlowManager.openFxAPairDevice(event.target.ownerGlobal); + this.pairDeviceCallback?.(); + break; + } + case "sync-tabs-disabled": { + TabsSetupFlowManager.syncOpenTabs(event.target); + break; + } + case `${ErrorType.SYNC_DISCONNECTED}`: { + const win = event.target.ownerGlobal; + const { switchToTabHavingURI } = + win.docShell.chromeEventHandler.ownerGlobal; + switchToTabHavingURI( + "about:preferences?action=choose-what-to-sync#sync", + true, + {} + ); + break; + } + } + } + } + + async observe(_, topic, errorState) { + if (topic == TOPIC_SETUPSTATE_CHANGED) { + await this.updateStates(errorState); + } + if (topic == SYNCED_TABS_CHANGED) { + await this.getSyncedTabData(); + } + } + + async updateStates(errorState) { + let stateIndex = TabsSetupFlowManager.uiStateIndex; + errorState = errorState || SyncedTabsErrorHandler.getErrorType(); + + if (stateIndex == 4 && this.currentSetupStateIndex !== stateIndex) { + // trigger an initial request for the synced tabs list + await this.getSyncedTabData(); + } + + this.currentSetupStateIndex = stateIndex; + this.errorState = errorState; + this.host.requestUpdate(); + } + + actionMappings = { + "sign-in": { + header: "firefoxview-syncedtabs-signin-header", + description: "firefoxview-syncedtabs-signin-description", + buttonLabel: "firefoxview-syncedtabs-signin-primarybutton", + }, + "add-device": { + header: "firefoxview-syncedtabs-adddevice-header", + description: "firefoxview-syncedtabs-adddevice-description", + buttonLabel: "firefoxview-syncedtabs-adddevice-primarybutton", + descriptionLink: { + name: "url", + url: "https://support.mozilla.org/kb/how-do-i-set-sync-my-computer#w_connect-additional-devices-to-sync", + }, + }, + "sync-tabs-disabled": { + header: "firefoxview-syncedtabs-synctabs-header", + description: "firefoxview-syncedtabs-synctabs-description", + buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton", + }, + loading: { + header: "firefoxview-syncedtabs-loading-header", + description: "firefoxview-syncedtabs-loading-description", + }, + }; + + #getMessageCardForState({ error = false, action, errorState }) { + errorState = errorState || this.errorState; + let header, + description, + descriptionLink, + buttonLabel, + headerIconUrl, + mainImageUrl; + let descriptionArray; + if (error) { + let link; + ({ header, description, link, buttonLabel } = + SyncedTabsErrorHandler.getFluentStringsForErrorType(errorState)); + action = `${errorState}`; + headerIconUrl = "chrome://global/skin/icons/info-filled.svg"; + mainImageUrl = + "chrome://browser/content/firefoxview/synced-tabs-error.svg"; + descriptionArray = [description]; + if (errorState == "password-locked") { + descriptionLink = {}; + // This is ugly, but we need to special case this link so we can + // coexist with the old view. + descriptionArray.push("firefoxview-syncedtab-password-locked-link"); + descriptionLink.name = "syncedtab-password-locked-link"; + descriptionLink.url = link.href; + } + } else { + header = this.actionMappings[action].header; + description = this.actionMappings[action].description; + buttonLabel = this.actionMappings[action].buttonLabel; + descriptionLink = this.actionMappings[action].descriptionLink; + mainImageUrl = + "chrome://browser/content/firefoxview/synced-tabs-error.svg"; + descriptionArray = [description]; + } + return { + action, + buttonLabel, + descriptionArray, + descriptionLink, + error, + header, + headerIconUrl, + mainImageUrl, + }; + } + + getRenderInfo() { + let renderInfo = {}; + for (let tab of this.currentSyncedTabs) { + if (!(tab.client in renderInfo)) { + renderInfo[tab.client] = { + name: tab.device, + deviceType: tab.deviceType, + tabs: [], + }; + } + renderInfo[tab.client].tabs.push(tab); + } + + // Add devices without tabs + for (let device of this.devices) { + if (!(device.id in renderInfo)) { + renderInfo[device.id] = { + name: device.name, + deviceType: device.clientType, + tabs: [], + }; + } + } + + for (let id in renderInfo) { + renderInfo[id].tabItems = this.searchQuery + ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs)) + : this.getTabItems(renderInfo[id].tabs); + } + return renderInfo; + } + + getMessageCard() { + switch (this.currentSetupStateIndex) { + case 0 /* error-state */: + if (this.errorState) { + return this.#getMessageCardForState({ error: true }); + } + return this.#getMessageCardForState({ action: "loading" }); + case 1 /* not-signed-in */: + if (Services.prefs.prefHasUserValue("services.sync.lastversion")) { + // If this pref is set, the user has signed out of sync. + // This path is also taken if we are disconnected from sync. See bug 1784055 + return this.#getMessageCardForState({ + error: true, + errorState: "signed-out", + }); + } + return this.#getMessageCardForState({ action: "sign-in" }); + case 2 /* connect-secondary-device*/: + return this.#getMessageCardForState({ action: "add-device" }); + case 3 /* disabled-tab-sync */: + return this.#getMessageCardForState({ action: "sync-tabs-disabled" }); + case 4 /* synced-tabs-loaded*/: + // There seems to be an edge case where sync says everything worked + // fine but we have no devices. + if (!this.devices.length) { + return this.#getMessageCardForState({ action: "add-device" }); + } + } + return null; + } + + getTabItems(tabs) { + return tabs?.map(tab => ({ + icon: tab.icon, + title: tab.title, + time: tab.lastUsed * 1000, + url: tab.url, + primaryL10nId: "firefoxview-tabs-list-tab-button", + primaryL10nArgs: JSON.stringify({ targetURI: tab.url }), + secondaryL10nId: this.contextMenu + ? "fxviewtabrow-options-menu-button" + : undefined, + secondaryL10nArgs: this.contextMenu + ? JSON.stringify({ tabTitle: tab.title }) + : undefined, + })); + } + + updateTabsList(syncedTabs) { + if (!syncedTabs.length) { + this.currentSyncedTabs = syncedTabs; + } + + const tabsToRender = syncedTabs; + + // Return early if new tabs are the same as previous ones + if (lazy.ObjectUtils.deepEqual(tabsToRender, this.currentSyncedTabs)) { + return; + } + + this.currentSyncedTabs = tabsToRender; + this.host.requestUpdate(); + } + + async getSyncedTabData() { + this.devices = await lazy.SyncedTabs.getTabClients(); + let tabs = await lazy.SyncedTabs.createRecentTabsList(this.devices, 50, { + removeAllDupes: false, + removeDeviceDupes: true, + }); + + this.updateTabsList(tabs); + } +} diff --git a/browser/components/firefoxview/card-container.css b/browser/components/firefoxview/card-container.css index 953437bec1..0c6a81899b 100644 --- a/browser/components/firefoxview/card-container.css +++ b/browser/components/firefoxview/card-container.css @@ -14,9 +14,9 @@ } } -@media (prefers-contrast) { +@media (forced-colors) or (prefers-contrast) { .card-container { - border: 1px solid CanvasText; + border: 1px solid var(--fxview-border); } } @@ -83,7 +83,7 @@ background-color: var(--fxview-element-background-hover); } -@media (prefers-contrast) { +@media (forced-colors) { .chevron-icon { border: 1px solid ButtonText; color: ButtonText; diff --git a/browser/components/firefoxview/card-container.mjs b/browser/components/firefoxview/card-container.mjs index b58f42204a..1755d97555 100644 --- a/browser/components/firefoxview/card-container.mjs +++ b/browser/components/firefoxview/card-container.mjs @@ -118,7 +118,7 @@ class CardContainer extends MozLitElement { } updateTabLists() { - let tabLists = this.querySelectorAll("fxview-tab-list"); + let tabLists = this.querySelectorAll("fxview-tab-list, opentabs-tab-list"); if (tabLists) { tabLists.forEach(tabList => { tabList.updatesPaused = !this.visible || !this.isExpanded; diff --git a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs index 4c43eea1b6..e1c999d89c 100644 --- a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs +++ b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs @@ -591,12 +591,6 @@ export const TabsSetupFlowManager = new (class { ); this.didFxaTabOpen = true; openTabInWindow(window, url, true); - Services.telemetry.recordEvent( - "firefoxview_next", - "fxa_continue", - "sync", - null - ); } async openFxAPairDevice(window) { @@ -605,18 +599,9 @@ export const TabsSetupFlowManager = new (class { }); this.didFxaTabOpen = true; openTabInWindow(window, url, true); - Services.telemetry.recordEvent( - "firefoxview_next", - "fxa_mobile", - "sync", - null, - { - has_devices: this.secondaryDeviceConnected.toString(), - } - ); } - syncOpenTabs(containerElem) { + syncOpenTabs() { // Flip the pref on. // The observer should trigger re-evaluating state and advance to next step Services.prefs.setBoolPref(SYNC_TABS_PREF, true); diff --git a/browser/components/firefoxview/firefoxview.css b/browser/components/firefoxview/firefoxview.css index 6811ca54c4..a91c90c39e 100644 --- a/browser/components/firefoxview/firefoxview.css +++ b/browser/components/firefoxview/firefoxview.css @@ -31,6 +31,17 @@ --newtab-background-color: #F9F9FB; --fxview-card-header-font-weight: 500; + + /* Make the attention dot color match the browser UI on Linux, and on HCM + * with a lightweight theme. */ + &[lwtheme] { + --attention-dot-color: light-dark(#2ac3a2, #54ffbd); + } + @media (-moz-platform: linux) { + &:not([lwtheme]) { + --attention-dot-color: AccentColor; + } + } } @media (prefers-color-scheme: dark) { @@ -47,7 +58,7 @@ } } -@media (prefers-contrast) { +@media (forced-colors) { :root { --fxview-element-background-hover: ButtonText; --fxview-element-background-active: ButtonText; @@ -59,6 +70,12 @@ } } +@media (prefers-contrast) { + :root { + --fxview-border: var(--border-color); + } +} + @media (max-width: 52rem) { :root { --fxview-sidebar-width: 82px; diff --git a/browser/components/firefoxview/firefoxview.html b/browser/components/firefoxview/firefoxview.html index 6fa0f59a8f..5bffb5a1d8 100644 --- a/browser/components/firefoxview/firefoxview.html +++ b/browser/components/firefoxview/firefoxview.html @@ -72,6 +72,7 @@ > </moz-page-nav-button> <moz-page-nav-button + class="sync-ui-item" view="syncedtabs" data-l10n-id="firefoxview-synced-tabs-nav" iconSrc="chrome://browser/content/firefoxview/view-syncedtabs.svg" @@ -95,7 +96,10 @@ <view-recentlyclosed slot="recentlyclosed"></view-recentlyclosed> </div> <div> - <view-syncedtabs slot="syncedtabs"></view-syncedtabs> + <view-syncedtabs + class="sync-ui-item" + slot="syncedtabs" + ></view-syncedtabs> </div> </view-recentbrowsing> <view-history name="history" type="page"></view-history> @@ -104,7 +108,11 @@ name="recentlyclosed" type="page" ></view-recentlyclosed> - <view-syncedtabs name="syncedtabs" type="page"></view-syncedtabs> + <view-syncedtabs + class="sync-ui-item" + name="syncedtabs" + type="page" + ></view-syncedtabs> </named-deck> </div> </main> diff --git a/browser/components/firefoxview/firefoxview.mjs b/browser/components/firefoxview/firefoxview.mjs index 3e61482cc0..e31536bc8b 100644 --- a/browser/components/firefoxview/firefoxview.mjs +++ b/browser/components/firefoxview/firefoxview.mjs @@ -80,6 +80,16 @@ async function updateSearchKeyboardShortcut() { searchKeyboardShortcut = key.toLocaleLowerCase(); } +function updateSyncVisibility() { + const syncEnabled = Services.prefs.getBoolPref( + "identity.fxaccounts.enabled", + false + ); + for (const el of document.querySelectorAll(".sync-ui-item")) { + el.hidden = !syncEnabled; + } +} + window.addEventListener("DOMContentLoaded", async () => { recordEnteredTelemetry(); @@ -106,6 +116,7 @@ window.addEventListener("DOMContentLoaded", async () => { onViewsDeckViewChange(); await updateSearchTextboxSize(); await updateSearchKeyboardShortcut(); + updateSyncVisibility(); if (Cu.isInAutomation) { Services.obs.notifyObservers(null, "firefoxview-entered"); @@ -150,12 +161,17 @@ window.addEventListener( document.body.textContent = ""; topChromeWindow.removeEventListener("command", onCommand); Services.obs.removeObserver(onLocalesChanged, "intl:app-locales-changed"); + Services.prefs.removeObserver( + "identity.fxaccounts.enabled", + updateSyncVisibility + ); }, { once: true } ); topChromeWindow.addEventListener("command", onCommand); Services.obs.addObserver(onLocalesChanged, "intl:app-locales-changed"); +Services.prefs.addObserver("identity.fxaccounts.enabled", updateSyncVisibility); function onCommand(e) { if (document.hidden || !e.target.closest("#contentAreaContextMenu")) { diff --git a/browser/components/firefoxview/fxview-empty-state.css b/browser/components/firefoxview/fxview-empty-state.css index 80b4099e6a..8c0d08c1f8 100644 --- a/browser/components/firefoxview/fxview-empty-state.css +++ b/browser/components/firefoxview/fxview-empty-state.css @@ -93,7 +93,7 @@ img.greyscale { filter: grayscale(100%); - @media not (prefers-contrast) { + @media not (forced-colors) { opacity: 0.5; } } diff --git a/browser/components/firefoxview/fxview-tab-list.css b/browser/components/firefoxview/fxview-tab-list.css index 5a4bff023a..f0881d8ce8 100644 --- a/browser/components/firefoxview/fxview-tab-list.css +++ b/browser/components/firefoxview/fxview-tab-list.css @@ -9,35 +9,21 @@ .fxview-tab-list { display: grid; - grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content; + grid-template-columns: min-content 3fr 2fr 1fr 1fr min-content; gap: var(--space-xsmall); - &.pinned { - display: flex; - flex-wrap: wrap; - - > virtual-list { - display: block; - } - - > fxview-tab-row { - display: block; - margin-block-end: var(--space-xsmall); - } - } - :host([compactRows]) & { - grid-template-columns: min-content 1fr min-content min-content min-content; + grid-template-columns: min-content 1fr min-content min-content; } } virtual-list { display: grid; - grid-column: span 9; + grid-column: span 7; grid-template-columns: subgrid; .top-padding, .bottom-padding { - grid-column: span 9; + grid-column: span 7; } } diff --git a/browser/components/firefoxview/fxview-tab-list.mjs b/browser/components/firefoxview/fxview-tab-list.mjs index 978ab79724..57181e3bea 100644 --- a/browser/components/firefoxview/fxview-tab-list.mjs +++ b/browser/components/firefoxview/fxview-tab-list.mjs @@ -12,6 +12,8 @@ import { } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; import { escapeRegExp } from "./helpers.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; const NOW_THRESHOLD_MS = 91000; const FXVIEW_ROW_HEIGHT_PX = 32; @@ -45,13 +47,13 @@ if (!window.IS_STORYBOOK) { * @property {string} dateTimeFormat - Expected format for date and/or time * @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required * @property {number} maxTabsLength - The max number of tabs for the list - * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view * @property {Array} tabItems - Items to show in the tab list * @property {string} searchQuery - The query string to highlight, if provided. + * @property {string} searchInProgress - Whether a search has been initiated. * @property {string} secondaryActionClass - The class used to style the secondary action element * @property {string} tertiaryActionClass - The class used to style the tertiary action element */ -export default class FxviewTabList extends MozLitElement { +export class FxviewTabListBase extends MozLitElement { constructor() { super(); window.MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl"); @@ -62,10 +64,8 @@ export default class FxviewTabList extends MozLitElement { this.dateTimeFormat = "relative"; this.maxTabsLength = 25; this.tabItems = []; - this.pinnedTabs = []; - this.pinnedTabsGridView = false; - this.unpinnedTabs = []; this.compactRows = false; + this.searchInProgress = false; this.updatesPaused = true; this.#register(); } @@ -77,16 +77,18 @@ export default class FxviewTabList extends MozLitElement { dateTimeFormat: { type: String }, hasPopup: { type: String }, maxTabsLength: { type: Number }, - pinnedTabsGridView: { type: Boolean }, tabItems: { type: Array }, updatesPaused: { type: Boolean }, searchQuery: { type: String }, + searchInProgress: { type: Boolean }, secondaryActionClass: { type: String }, tertiaryActionClass: { type: String }, }; static queries = { - rowEls: { all: "fxview-tab-row" }, + rowEls: { + all: "fxview-tab-row", + }, rootVirtualListEl: "virtual-list", }; @@ -108,20 +110,7 @@ export default class FxviewTabList extends MozLitElement { } } - // Move pinned tabs to the beginning of the list - if (this.pinnedTabsGridView) { - // Can set maxTabsLength to -1 to have no max - this.unpinnedTabs = this.tabItems.filter( - tab => !tab.indicators?.includes("pinned") - ); - this.pinnedTabs = this.tabItems.filter(tab => - tab.indicators?.includes("pinned") - ); - if (this.maxTabsLength > 0) { - this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength); - } - this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs]; - } else if (this.maxTabsLength > 0) { + if (this.maxTabsLength > 0) { this.tabItems = this.tabItems.slice(0, this.maxTabsLength); } } @@ -148,7 +137,7 @@ export default class FxviewTabList extends MozLitElement { "timeMsPref", "browser.tabs.firefox-view.updateTimeMs", NOW_THRESHOLD_MS, - (prefName, oldVal, newVal) => { + () => { this.clearIntervalTimer(); if (!this.isConnected) { return; @@ -197,93 +186,32 @@ export default class FxviewTabList extends MozLitElement { if (e.code == "ArrowUp") { // Focus either the link or button of the previous row based on this.currentActiveElementId e.preventDefault(); - if ( - (this.pinnedTabsGridView && - this.activeIndex >= this.pinnedTabs.length) || - !this.pinnedTabsGridView - ) { - this.focusPrevRow(); - } + this.focusPrevRow(); } else if (e.code == "ArrowDown") { // Focus either the link or button of the next row based on this.currentActiveElementId e.preventDefault(); - if ( - this.pinnedTabsGridView && - this.activeIndex < this.pinnedTabs.length - ) { - this.focusIndex(this.pinnedTabs.length); - } else { - this.focusNextRow(); - } + this.focusNextRow(); } else if (e.code == "ArrowRight") { // Focus either the link or the button in the current row and // set this.currentActiveElementId to that element's ID e.preventDefault(); if (document.dir == "rtl") { - this.moveFocusLeft(fxviewTabRow); + fxviewTabRow.moveFocusLeft(); } else { - this.moveFocusRight(fxviewTabRow); + fxviewTabRow.moveFocusRight(); } } else if (e.code == "ArrowLeft") { // Focus either the link or the button in the current row and // set this.currentActiveElementId to that element's ID e.preventDefault(); if (document.dir == "rtl") { - this.moveFocusRight(fxviewTabRow); + fxviewTabRow.moveFocusRight(); } else { - this.moveFocusLeft(fxviewTabRow); + fxviewTabRow.moveFocusLeft(); } } } - moveFocusRight(fxviewTabRow) { - if ( - this.pinnedTabsGridView && - fxviewTabRow.indicators?.includes("pinned") - ) { - this.focusNextRow(); - } else if ( - (fxviewTabRow.indicators?.includes("soundplaying") || - fxviewTabRow.indicators?.includes("muted")) && - this.currentActiveElementId === "fxview-tab-row-main" - ) { - this.currentActiveElementId = fxviewTabRow.focusMediaButton(); - } else if ( - this.currentActiveElementId === "fxview-tab-row-media-button" || - this.currentActiveElementId === "fxview-tab-row-main" - ) { - this.currentActiveElementId = fxviewTabRow.focusSecondaryButton(); - } else if ( - fxviewTabRow.tertiaryButtonEl && - this.currentActiveElementId === "fxview-tab-row-secondary-button" - ) { - this.currentActiveElementId = fxviewTabRow.focusTertiaryButton(); - } - } - - moveFocusLeft(fxviewTabRow) { - if ( - this.pinnedTabsGridView && - (fxviewTabRow.indicators?.includes("pinned") || - (this.currentActiveElementId === "fxview-tab-row-main" && - this.activeIndex === this.pinnedTabs.length)) - ) { - this.focusPrevRow(); - } else if ( - this.currentActiveElementId === "fxview-tab-row-tertiary-button" - ) { - this.currentActiveElementId = fxviewTabRow.focusSecondaryButton(); - } else if ( - (fxviewTabRow.indicators?.includes("soundplaying") || - fxviewTabRow.indicators?.includes("muted")) && - this.currentActiveElementId === "fxview-tab-row-secondary-button" - ) { - this.currentActiveElementId = fxviewTabRow.focusMediaButton(); - } else { - this.currentActiveElementId = fxviewTabRow.focusLink(); - } - } - focusPrevRow() { this.focusIndex(this.activeIndex - 1); } @@ -294,18 +222,12 @@ export default class FxviewTabList extends MozLitElement { async focusIndex(index) { // Focus link or button of item - if ( - ((this.pinnedTabsGridView && index > this.pinnedTabs.length) || - !this.pinnedTabsGridView) && - lazy.virtualListEnabledPref - ) { - let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length); + if (lazy.virtualListEnabledPref) { + let row = this.rootVirtualListEl.getItem(index); if (!row) { return; } - let subList = this.rootVirtualListEl.getSubListForItem( - index - this.pinnedTabs.length - ); + let subList = this.rootVirtualListEl.getSubListForItem(index); if (!subList) { return; } @@ -347,27 +269,15 @@ export default class FxviewTabList extends MozLitElement { time = tabItem.time || tabItem.closedAt; } } + return html` <fxview-tab-row - exportparts="secondary-button" - class=${classMap({ - pinned: - this.pinnedTabsGridView && tabItem.indicators?.includes("pinned"), - })} ?active=${i == this.activeIndex} ?compact=${this.compactRows} - .hasPopup=${this.hasPopup} - .containerObj=${ifDefined(tabItem.containerObj)} .currentActiveElementId=${this.currentActiveElementId} - .dateTimeFormat=${this.dateTimeFormat} .favicon=${tabItem.icon} - .indicators=${ifDefined(tabItem.indicators)} - .pinnedTabsGridView=${ifDefined(this.pinnedTabsGridView)} .primaryL10nId=${tabItem.primaryL10nId} .primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)} - role=${this.pinnedTabsGridView && tabItem.indicators?.includes("pinned") - ? "none" - : "listitem"} .secondaryL10nId=${tabItem.secondaryL10nId} .secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)} .tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)} @@ -377,41 +287,36 @@ export default class FxviewTabList extends MozLitElement { .sourceClosedId=${ifDefined(tabItem.sourceClosedId)} .sourceWindowId=${ifDefined(tabItem.sourceWindowId)} .closedId=${ifDefined(tabItem.closedId || tabItem.closedId)} - .searchQuery=${ifDefined(this.searchQuery)} + role="listitem" .tabElement=${ifDefined(tabItem.tabElement)} .time=${ifDefined(time)} - .timeMsPref=${ifDefined(this.timeMsPref)} .title=${tabItem.title} .url=${tabItem.url} + .searchQuery=${ifDefined(this.searchQuery)} + .timeMsPref=${ifDefined(this.timeMsPref)} + .hasPopup=${this.hasPopup} + .dateTimeFormat=${this.dateTimeFormat} ></fxview-tab-row> `; }; + stylesheets() { + return html`<link + rel="stylesheet" + href="chrome://browser/content/firefoxview/fxview-tab-list.css" + />`; + } + render() { - if (this.searchQuery && this.tabItems.length === 0) { - return this.#emptySearchResultsTemplate(); + if ( + this.searchQuery && + this.tabItems.length === 0 && + !this.searchInProgress + ) { + return this.emptySearchResultsTemplate(); } return html` - <link - rel="stylesheet" - href="chrome://browser/content/firefoxview/fxview-tab-list.css" - /> - ${when( - this.pinnedTabsGridView && this.pinnedTabs.length, - () => html` - <div - id="fxview-tab-list" - class="fxview-tab-list pinned" - data-l10n-id="firefoxview-pinned-tabs" - role="tablist" - @keydown=${this.handleFocusElementInRow} - > - ${this.pinnedTabs.map((tabItem, i) => - this.itemTemplate(tabItem, i) - )} - </div> - ` - )} + ${this.stylesheets()} <div id="fxview-tab-list" class="fxview-tab-list" @@ -424,28 +329,21 @@ export default class FxviewTabList extends MozLitElement { () => html` <virtual-list .activeIndex=${this.activeIndex} - .pinnedTabsIndexOffset=${this.pinnedTabsGridView - ? this.pinnedTabs.length - : 0} - .items=${this.pinnedTabsGridView - ? this.unpinnedTabs - : this.tabItems} + .items=${this.tabItems} .template=${this.itemTemplate} ></virtual-list> - ` - )} - ${when( - !lazy.virtualListEnabledPref, - () => html` - ${this.tabItems.map((tabItem, i) => this.itemTemplate(tabItem, i))} - ` + `, + () => + html`${this.tabItems.map((tabItem, i) => + this.itemTemplate(tabItem, i) + )}` )} </div> <slot name="menu"></slot> `; } - #emptySearchResultsTemplate() { + emptySearchResultsTemplate() { return html` <fxview-empty-state class="search-results" headerLabel="firefoxview-search-results-empty" @@ -455,23 +353,20 @@ export default class FxviewTabList extends MozLitElement { </fxview-empty-state>`; } } -customElements.define("fxview-tab-list", FxviewTabList); +customElements.define("fxview-tab-list", FxviewTabListBase); /** * A tab item that displays favicon, title, url, and time of last access * * @property {boolean} active - Should current item have focus on keydown * @property {boolean} compact - Whether to hide the URL and date/time for this tab. - * @property {object} containerObj - Info about an open tab's container if within one * @property {string} currentActiveElementId - ID of currently focused element within each tab item * @property {string} dateTimeFormat - Expected format for date and/or time * @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required - * @property {string} indicators - An array of tab indicators if any are present * @property {number} closedId - The tab ID for when the tab item was closed. * @property {number} sourceClosedId - The closedId of the closed window its from if applicable * @property {number} sourceWindowId - The sessionstore id of the window its from if applicable * @property {string} favicon - The favicon for the tab item. - * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view * @property {string} primaryL10nId - The l10n id used for the primary action element * @property {string} primaryL10nArgs - The l10n args used for the primary action element * @property {string} secondaryL10nId - The l10n id used for the secondary action button @@ -487,23 +382,14 @@ customElements.define("fxview-tab-list", FxviewTabList); * @property {number} timeMsPref - The frequency in milliseconds of updates to relative time * @property {string} searchQuery - The query string to highlight, if provided. */ -export class FxviewTabRow extends MozLitElement { - constructor() { - super(); - this.active = false; - this.currentActiveElementId = "fxview-tab-row-main"; - } - +export class FxviewTabRowBase extends MozLitElement { static properties = { active: { type: Boolean }, compact: { type: Boolean }, - containerObj: { type: Object }, currentActiveElementId: { type: String }, dateTimeFormat: { type: String }, favicon: { type: String }, hasPopup: { type: String }, - indicators: { type: Array }, - pinnedTabsGridView: { type: Boolean }, primaryL10nId: { type: String }, primaryL10nArgs: { type: String }, secondaryL10nId: { type: String }, @@ -523,12 +409,16 @@ export class FxviewTabRow extends MozLitElement { searchQuery: { type: String }, }; + constructor() { + super(); + this.active = false; + this.currentActiveElementId = "fxview-tab-row-main"; + } + static queries = { mainEl: "#fxview-tab-row-main", secondaryButtonEl: "#fxview-tab-row-secondary-button:not([hidden])", tertiaryButtonEl: "#fxview-tab-row-tertiary-button", - mediaButtonEl: "#fxview-tab-row-media-button", - pinnedTabButtonEl: "button#fxview-tab-row-main", }; get currentFocusable() { @@ -539,50 +429,45 @@ export class FxviewTabRow extends MozLitElement { return focusItem; } - connectedCallback() { - super.connectedCallback(); - this.addEventListener("keydown", this.handleKeydown); - } - - disconnectedCallback() { - super.disconnectedCallback(); - this.removeEventListener("keydown", this.handleKeydown); - } - - handleKeydown(e) { - if ( - this.active && - this.pinnedTabsGridView && - this.indicators?.includes("pinned") && - e.key === "m" && - e.ctrlKey - ) { - this.muteOrUnmuteTab(); - } - } - focus() { this.currentFocusable.focus(); } focusSecondaryButton() { + let tabList = this.getRootNode().host; this.secondaryButtonEl.focus(); - return this.secondaryButtonEl.id; + tabList.currentActiveElementId = this.secondaryButtonEl.id; } focusTertiaryButton() { + let tabList = this.getRootNode().host; this.tertiaryButtonEl.focus(); - return this.tertiaryButtonEl.id; - } - - focusMediaButton() { - this.mediaButtonEl.focus(); - return this.mediaButtonEl.id; + tabList.currentActiveElementId = this.tertiaryButtonEl.id; } focusLink() { + let tabList = this.getRootNode().host; this.mainEl.focus(); - return this.mainEl.id; + tabList.currentActiveElementId = this.mainEl.id; + } + + moveFocusRight() { + if (this.currentActiveElementId === "fxview-tab-row-main") { + this.focusSecondaryButton(); + } else if ( + this.tertiaryButtonEl && + this.currentActiveElementId === "fxview-tab-row-secondary-button" + ) { + this.focusTertiaryButton(); + } + } + + moveFocusLeft() { + if (this.currentActiveElementId === "fxview-tab-row-tertiary-button") { + this.focusSecondaryButton(); + } else { + this.focusLink(); + } } dateFluentArgs(timestamp, dateTimeFormat) { @@ -652,16 +537,6 @@ export class FxviewTabRow extends MozLitElement { return icon; } - getContainerClasses() { - let containerClasses = ["fxview-tab-row-container-indicator", "icon"]; - if (this.containerObj) { - let { icon, color } = this.containerObj; - containerClasses.push(`identity-icon-${icon}`); - containerClasses.push(`identity-color-${color}`); - } - return containerClasses; - } - primaryActionHandler(event) { if ( (event.type == "click" && !event.altKey) || @@ -683,9 +558,6 @@ export class FxviewTabRow extends MozLitElement { secondaryActionHandler(event) { if ( - (this.pinnedTabsGridView && - this.indicators?.includes("pinned") && - event.type == "contextmenu") || (event.type == "click" && event.detail && !event.altKey) || // detail=0 is from keyboard (event.type == "click" && !event.detail) @@ -718,92 +590,80 @@ export class FxviewTabRow extends MozLitElement { } } - muteOrUnmuteTab(e) { - e?.preventDefault(); - // If the tab has no sound playing, the mute/unmute button will be removed when toggled. - // We should move the focus to the right in that case. This does not apply to pinned tabs - // on the Open Tabs page. - let shouldMoveFocus = - (!this.pinnedTabsGridView || - (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) && - this.mediaButtonEl && - !this.indicators.includes("soundplaying") && - this.currentActiveElementId === "fxview-tab-row-media-button"; - - // detail=0 is from keyboard - if (e?.type == "click" && !e?.detail && shouldMoveFocus) { - let tabList = this.getRootNode().host; - if (document.dir == "rtl") { - tabList.moveFocusLeft(this); - } else { - tabList.moveFocusRight(this); - } + /** + * Find all matches of query within the given string, and compute the result + * to be rendered. + * + * @param {string} query + * @param {string} string + */ + highlightSearchMatches(query, string) { + const fragments = []; + const regex = RegExp(escapeRegExp(query), "dgi"); + let prevIndexEnd = 0; + let result; + while ((result = regex.exec(string)) !== null) { + const [indexStart, indexEnd] = result.indices[0]; + fragments.push(string.substring(prevIndexEnd, indexStart)); + fragments.push( + html`<strong>${string.substring(indexStart, indexEnd)}</strong>` + ); + prevIndexEnd = regex.lastIndex; } - this.tabElement.toggleMuteAudio(); + fragments.push(string.substring(prevIndexEnd)); + return fragments; + } + + stylesheets() { + return html`<link + rel="stylesheet" + href="chrome://browser/content/firefoxview/fxview-tab-row.css" + />`; } - #faviconTemplate() { + faviconTemplate() { return html`<span - class="${classMap({ - "fxview-tab-row-favicon-wrapper": true, - pinned: this.indicators?.includes("pinned"), - pinnedOnNewTab: this.indicators?.includes("pinnedOnNewTab"), - attention: this.indicators?.includes("attention"), - bookmark: this.indicators?.includes("bookmark"), - })}" + class="fxview-tab-row-favicon icon" + id="fxview-tab-row-favicon" + style=${styleMap({ + backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`, + })} + ></span>`; + } + + titleTemplate() { + const title = this.title; + return html`<span + class="fxview-tab-row-title text-truncated-ellipsis" + id="fxview-tab-row-title" + dir="auto" > - <span - class="fxview-tab-row-favicon icon" - id="fxview-tab-row-favicon" - style=${styleMap({ - backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`, - })} - ></span> ${when( - this.pinnedTabsGridView && - this.indicators?.includes("pinned") && - (this.indicators?.includes("muted") || - this.indicators?.includes("soundplaying")), - () => html` - <button - class="fxview-tab-row-pinned-media-button ghost-button icon-button" - id="fxview-tab-row-media-button" - tabindex="-1" - data-l10n-id=${this.indicators?.includes("muted") - ? "fxviewtabrow-unmute-tab-button-no-context" - : "fxviewtabrow-mute-tab-button-no-context"} - muted=${this.indicators?.includes("muted")} - soundplaying=${this.indicators?.includes("soundplaying") && - !this.indicators?.includes("muted")} - @click=${this.muteOrUnmuteTab} - ></button> - ` + this.searchQuery, + () => this.highlightSearchMatches(this.searchQuery, title), + () => title )} </span>`; } - #pinnedTabItemTemplate() { - return html` <button - class="fxview-tab-row-main ghost-button semi-transparent" - id="fxview-tab-row-main" - aria-haspopup=${ifDefined(this.hasPopup)} - data-l10n-id=${ifDefined(this.primaryL10nId)} - data-l10n-args=${ifDefined(this.primaryL10nArgs)} - tabindex=${this.active && - this.currentActiveElementId === "fxview-tab-row-main" - ? "0" - : "-1"} - role="tab" - @click=${this.primaryActionHandler} - @keydown=${this.primaryActionHandler} - @contextmenu=${this.secondaryActionHandler} + urlTemplate() { + return html`<span + class="fxview-tab-row-url text-truncated-ellipsis" + id="fxview-tab-row-url" > - ${this.#faviconTemplate()} - </button>`; + ${when( + this.searchQuery, + () => + this.highlightSearchMatches( + this.searchQuery, + this.formatURIForDisplay(this.url) + ), + () => this.formatURIForDisplay(this.url) + )} + </span>`; } - #unpinnedTabItemTemplate() { - const title = this.title; + dateTemplate() { const relativeString = this.relativeTime( this.time, this.dateTimeFormat, @@ -815,11 +675,81 @@ export class FxviewTabRow extends MozLitElement { !window.IS_STORYBOOK ? this.timeMsPref : NOW_THRESHOLD_MS ); const dateArgs = this.dateFluentArgs(this.time, this.dateTimeFormat); + return html`<span class="fxview-tab-row-date" id="fxview-tab-row-date"> + <span + ?hidden=${relativeString || !dateString} + data-l10n-id=${ifDefined(dateString)} + data-l10n-args=${ifDefined(dateArgs)} + ></span> + <span ?hidden=${!relativeString}>${relativeString}</span> + </span>`; + } + + timeTemplate() { const timeString = this.timeFluentId(this.dateTimeFormat); const time = this.time; const timeArgs = JSON.stringify({ time }); + return html`<span + class="fxview-tab-row-time" + id="fxview-tab-row-time" + ?hidden=${!timeString} + data-timestamp=${ifDefined(this.time)} + data-l10n-id=${ifDefined(timeString)} + data-l10n-args=${ifDefined(timeArgs)} + > + </span>`; + } - return html`<a + secondaryButtonTemplate() { + return html`${when( + this.secondaryL10nId && this.secondaryActionHandler, + () => html`<moz-button + type="icon ghost" + class=${classMap({ + "fxview-tab-row-button": true, + [this.secondaryActionClass]: this.secondaryActionClass, + })} + id="fxview-tab-row-secondary-button" + data-l10n-id=${this.secondaryL10nId} + data-l10n-args=${ifDefined(this.secondaryL10nArgs)} + aria-haspopup=${ifDefined(this.hasPopup)} + @click=${this.secondaryActionHandler} + tabindex="${this.active && + this.currentActiveElementId === "fxview-tab-row-secondary-button" + ? "0" + : "-1"}" + ></moz-button>` + )}`; + } + + tertiaryButtonTemplate() { + return html`${when( + this.tertiaryL10nId && this.tertiaryActionHandler, + () => html`<moz-button + type="icon ghost" + class=${classMap({ + "fxview-tab-row-button": true, + [this.tertiaryActionClass]: this.tertiaryActionClass, + })} + id="fxview-tab-row-tertiary-button" + data-l10n-id=${this.tertiaryL10nId} + data-l10n-args=${ifDefined(this.tertiaryL10nArgs)} + aria-haspopup=${ifDefined(this.hasPopup)} + @click=${this.tertiaryActionHandler} + tabindex="${this.active && + this.currentActiveElementId === "fxview-tab-row-tertiary-button" + ? "0" + : "-1"}" + ></moz-button>` + )}`; + } +} + +export class FxviewTabRow extends FxviewTabRowBase { + render() { + return html` + ${this.stylesheets()} + <a href=${ifDefined(this.url)} class="fxview-tab-row-main" id="fxview-tab-row-main" @@ -833,176 +763,16 @@ export class FxviewTabRow extends MozLitElement { @keydown=${this.primaryActionHandler} title=${!this.primaryL10nId ? this.url : null} > - ${this.#faviconTemplate()} - <span - class="fxview-tab-row-title text-truncated-ellipsis" - id="fxview-tab-row-title" - dir="auto" - > - ${when( - this.searchQuery, - () => this.#highlightSearchMatches(this.searchQuery, title), - () => title - )} - </span> - <span class=${this.getContainerClasses().join(" ")}></span> - <span - class="fxview-tab-row-url text-truncated-ellipsis" - id="fxview-tab-row-url" - ?hidden=${this.compact} - > - ${when( - this.searchQuery, - () => - this.#highlightSearchMatches( - this.searchQuery, - this.formatURIForDisplay(this.url) - ), - () => this.formatURIForDisplay(this.url) - )} - </span> - <span - class="fxview-tab-row-date" - id="fxview-tab-row-date" - ?hidden=${this.compact} - > - <span - ?hidden=${relativeString || !dateString} - data-l10n-id=${ifDefined(dateString)} - data-l10n-args=${ifDefined(dateArgs)} - ></span> - <span ?hidden=${!relativeString}>${relativeString}</span> - </span> - <span - class="fxview-tab-row-time" - id="fxview-tab-row-time" - ?hidden=${this.compact || !timeString} - data-timestamp=${ifDefined(this.time)} - data-l10n-id=${ifDefined(timeString)} - data-l10n-args=${ifDefined(timeArgs)} - > - </span> + ${this.faviconTemplate()} ${this.titleTemplate()} + ${when( + !this.compact, + () => html`${this.urlTemplate()} ${this.dateTemplate()} + ${this.timeTemplate()}` + )} </a> - ${when( - this.indicators?.includes("soundplaying") || - this.indicators?.includes("muted"), - () => html`<button - class=fxview-tab-row-button ghost-button icon-button semi-transparent" - id="fxview-tab-row-media-button" - data-l10n-id=${ - this.indicators?.includes("muted") - ? "fxviewtabrow-unmute-tab-button-no-context" - : "fxviewtabrow-mute-tab-button-no-context" - } - muted=${this.indicators?.includes("muted")} - soundplaying=${ - this.indicators?.includes("soundplaying") && - !this.indicators?.includes("muted") - } - @click=${this.muteOrUnmuteTab} - tabindex="${ - this.active && - this.currentActiveElementId === "fxview-tab-row-media-button" - ? "0" - : "-1" - }" - ></button>`, - () => html`<span></span>` - )} - ${when( - this.secondaryL10nId && this.secondaryActionHandler, - () => html`<button - class=${classMap({ - "fxview-tab-row-button": true, - "ghost-button": true, - "icon-button": true, - "semi-transparent": true, - [this.secondaryActionClass]: this.secondaryActionClass, - })} - id="fxview-tab-row-secondary-button" - data-l10n-id=${this.secondaryL10nId} - data-l10n-args=${ifDefined(this.secondaryL10nArgs)} - aria-haspopup=${ifDefined(this.hasPopup)} - @click=${this.secondaryActionHandler} - tabindex="${this.active && - this.currentActiveElementId === "fxview-tab-row-secondary-button" - ? "0" - : "-1"}" - ></button>` - )} - ${when( - this.tertiaryL10nId && this.tertiaryActionHandler, - () => html`<button - class=${classMap({ - "fxview-tab-row-button": true, - "ghost-button": true, - "icon-button": true, - "semi-transparent": true, - [this.tertiaryActionClass]: this.tertiaryActionClass, - })} - id="fxview-tab-row-tertiary-button" - data-l10n-id=${this.tertiaryL10nId} - data-l10n-args=${ifDefined(this.tertiaryL10nArgs)} - aria-haspopup=${ifDefined(this.hasPopup)} - @click=${this.tertiaryActionHandler} - tabindex="${this.active && - this.currentActiveElementId === "fxview-tab-row-tertiary-button" - ? "0" - : "-1"}" - ></button>` - )}`; - } - - render() { - return html` - ${when( - this.containerObj, - () => html` - <link - rel="stylesheet" - href="chrome://browser/content/usercontext/usercontext.css" - /> - ` - )} - <link - rel="stylesheet" - href="chrome://global/skin/in-content/common.css" - /> - <link - rel="stylesheet" - href="chrome://browser/content/firefoxview/fxview-tab-row.css" - /> - ${when( - this.pinnedTabsGridView && this.indicators?.includes("pinned"), - this.#pinnedTabItemTemplate.bind(this), - this.#unpinnedTabItemTemplate.bind(this) - )} + ${this.secondaryButtonTemplate()} ${this.tertiaryButtonTemplate()} `; } - - /** - * Find all matches of query within the given string, and compute the result - * to be rendered. - * - * @param {string} query - * @param {string} string - */ - #highlightSearchMatches(query, string) { - const fragments = []; - const regex = RegExp(escapeRegExp(query), "dgi"); - let prevIndexEnd = 0; - let result; - while ((result = regex.exec(string)) !== null) { - const [indexStart, indexEnd] = result.indices[0]; - fragments.push(string.substring(prevIndexEnd, indexStart)); - fragments.push( - html`<strong>${string.substring(indexStart, indexEnd)}</strong>` - ); - prevIndexEnd = regex.lastIndex; - } - fragments.push(string.substring(prevIndexEnd)); - return fragments; - } } customElements.define("fxview-tab-row", FxviewTabRow); @@ -1040,10 +810,16 @@ export class VirtualList extends MozLitElement { this.isSubList = false; this.isVisible = false; this.intersectionObserver = new IntersectionObserver( - ([entry]) => (this.isVisible = entry.isIntersecting), + ([entry]) => { + this.isVisible = entry.isIntersecting; + }, { root: this.ownerDocument } ); - this.resizeObserver = new ResizeObserver(([entry]) => { + this.selfResizeObserver = new ResizeObserver(() => { + // Trigger the intersection observer once the tab rows have rendered + this.triggerIntersectionObserver(); + }); + this.childResizeObserver = new ResizeObserver(([entry]) => { if (entry.contentRect?.height > 0) { // Update properties on top-level virtual-list this.parentElement.itemHeightEstimate = entry.contentRect.height; @@ -1058,7 +834,8 @@ export class VirtualList extends MozLitElement { disconnectedCallback() { super.disconnectedCallback(); this.intersectionObserver.disconnect(); - this.resizeObserver.disconnect(); + this.childResizeObserver.disconnect(); + this.selfResizeObserver.disconnect(); } triggerIntersectionObserver() { @@ -1090,7 +867,6 @@ export class VirtualList extends MozLitElement { this.items.slice(i, i + this.maxRenderCountEstimate) ); } - this.triggerIntersectionObserver(); } } @@ -1103,13 +879,17 @@ export class VirtualList extends MozLitElement { firstUpdated() { this.intersectionObserver.observe(this); + this.selfResizeObserver.observe(this); if (this.isSubList && this.children[0]) { - this.resizeObserver.observe(this.children[0]); + this.childResizeObserver.observe(this.children[0]); } } updated(changedProperties) { this.updateListHeight(changedProperties); + if (changedProperties.has("items") && !this.isSubList) { + this.triggerIntersectionObserver(); + } } updateListHeight(changedProperties) { @@ -1157,5 +937,4 @@ export class VirtualList extends MozLitElement { return ""; } } - customElements.define("virtual-list", VirtualList); diff --git a/browser/components/firefoxview/fxview-tab-row.css b/browser/components/firefoxview/fxview-tab-row.css index 219d7e8aa2..c1c8f967a7 100644 --- a/browser/components/firefoxview/fxview-tab-row.css +++ b/browser/components/firefoxview/fxview-tab-row.css @@ -2,9 +2,11 @@ * 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 url("chrome://global/skin/design-system/text-and-typography.css"); + :host { - --fxviewtabrow-element-background-hover: color-mix(in srgb, currentColor 14%, transparent); - --fxviewtabrow-element-background-active: color-mix(in srgb, currentColor 21%, transparent); + --fxviewtabrow-element-background-hover: var(--button-background-color-ghost-hover); + --fxviewtabrow-element-background-active: var(--button-background-color-ghost-active); display: grid; grid-template-columns: subgrid; grid-column: span 9; @@ -12,7 +14,7 @@ border-radius: 4px; } -@media (prefers-contrast) { +@media (forced-colors) { :host { --fxviewtabrow-element-background-hover: ButtonText; --fxviewtabrow-element-background-active: ButtonText; @@ -32,115 +34,42 @@ cursor: pointer; text-decoration: none; - :host(.pinned) & { - padding: var(--space-small); - min-width: unset; - margin: 0; + :host([compact]) & { + grid-template-columns: min-content auto; } } .fxview-tab-row-main, .fxview-tab-row-main:visited, -.fxview-tab-row-main:hover:active, -.fxview-tab-row-button { +.fxview-tab-row-main:hover:active { color: inherit; } -.fxview-tab-row-main:hover, -.fxview-tab-row-button.ghost-button.icon-button:enabled:hover { +.fxview-tab-row-main:hover { background-color: var(--fxviewtabrow-element-background-hover); color: var(--fxviewtabrow-text-color-hover); - - & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after { - stroke: var(--fxview-indicator-stroke-color-hover); - } } -.fxview-tab-row-main:hover:active, -.fxview-tab-row-button.ghost-button.icon-button:enabled:hover:active { +.fxview-tab-row-main:hover:active { background-color: var(--fxviewtabrow-element-background-active); } -@media (prefers-contrast) { - a.fxview-tab-row-main, - a.fxview-tab-row-main:hover, - a.fxview-tab-row-main:active { +@media (forced-colors) { + .fxview-tab-row-main, + .fxview-tab-row-main:hover, + .fxview-tab-row-main:active { background-color: transparent; border: 1px solid LinkText; color: LinkText; } - a.fxview-tab-row-main:visited, - a.fxview-tab-row-main:visited:hover { + .fxview-tab-row-main:visited, + .fxview-tab-row-main:visited:hover { border: 1px solid VisitedText; color: VisitedText; } } -.fxview-tab-row-favicon-wrapper { - height: 16px; - position: relative; - - .fxview-tab-row-favicon::after, - .fxview-tab-row-button::after, - &.pinned .fxview-tab-row-pinned-media-button { - display: block; - content: ""; - background-size: 12px; - background-position: center; - background-repeat: no-repeat; - position: relative; - height: 12px; - width: 12px; - -moz-context-properties: fill, stroke; - fill: currentColor; - stroke: var(--fxview-background-color-secondary); - } - - &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after { - inset-block-start: 9px; - inset-inline-end: -6px; - } - - &.pinnedOnNewTab .fxview-tab-row-favicon::after, - &.pinnedOnNewTab .fxview-tab-row-button::after { - background-image: url("chrome://browser/skin/pin-12.svg"); - } - - &.bookmark .fxview-tab-row-favicon::after, - &.bookmark .fxview-tab-row-button::after { - background-image: url("chrome://browser/skin/bookmark-12.svg"); - fill: var(--fxview-primary-action-background); - } - - &.attention .fxview-tab-row-favicon::after, - &.attention .fxview-tab-row-button::after { - background-image: radial-gradient(circle, light-dark(rgb(42, 195, 162), rgb(84, 255, 189)), light-dark(rgb(42, 195, 162), rgb(84, 255, 189)) 2px, transparent 2px); - height: 4px; - width: 100%; - inset-block-start: 20px; - } - - &.pinned .fxview-tab-row-pinned-media-button { - inset-block-start: -10px; - inset-inline-end: -10px; - border-radius: 100%; - background-color: var(--fxview-background-color-secondary); - padding: 6px; - min-width: 0; - min-height: 0; - position: absolute; - - &[muted="true"] { - background-image: url("chrome://global/skin/media/audio-muted.svg"); - } - - &[soundplaying="true"] { - background-image: url("chrome://global/skin/media/audio.svg"); - } - } -} - .fxview-tab-row-favicon { background-size: cover; -moz-context-properties: fill; @@ -155,15 +84,6 @@ text-align: match-parent; } -.fxview-tab-row-container-indicator { - height: 16px; - width: 16px; - background-image: var(--identity-icon); - background-size: cover; - -moz-context-properties: fill; - fill: var(--identity-icon-color); -} - .fxview-tab-row-url { color: var(--text-color-deemphasized); text-decoration-line: underline; @@ -182,62 +102,22 @@ font-weight: 400; } -.fxview-tab-row-button { - margin: 0; - cursor: pointer; - min-width: 0; - background-color: transparent; - - &[muted="true"], - &[soundplaying="true"] { - background-size: 16px; - background-repeat: no-repeat; - background-position: center; - -moz-context-properties: fill; - fill: currentColor; - } - - &[muted="true"] { - background-image: url("chrome://global/skin/media/audio-muted.svg"); - } - - &[soundplaying="true"] { - background-image: url("chrome://global/skin/media/audio.svg"); - } - - &.dismiss-button { - background-image: url("chrome://global/skin/icons/close.svg"); - } - - &.options-button { - background-image: url("chrome://global/skin/icons/more.svg"); - } +.fxview-tab-row-button::part(button) { + color: var(--fxview-text-primary-color) } -@media (prefers-contrast) { - .fxview-tab-row-button, - button.fxview-tab-row-main { - border: 1px solid ButtonText; - color: ButtonText; - } +.fxview-tab-row-button[muted="true"]::part(button) { + background-image: url("chrome://global/skin/media/audio-muted.svg"); +} - .fxview-tab-row-button.ghost-button.icon-button:enabled:hover, - button.fxview-tab-row-main:enabled:hover { - border: 1px solid SelectedItem; - color: SelectedItem; - } +.fxview-tab-row-button[soundplaying="true"]::part(button) { + background-image: url("chrome://global/skin/media/audio.svg"); +} - .fxview-tab-row-button.ghost-button.icon-button:enabled:active, - button.fxview-tab-row-main:enabled:active { - color: SelectedItem; - } +.fxview-tab-row-button.dismiss-button::part(button) { + background-image: url("chrome://global/skin/icons/close.svg"); +} - .fxview-tab-row-button.ghost-button.icon-button:enabled, - .fxview-tab-row-button.ghost-button.icon-button:enabled:hover, - .fxview-tab-row-button.ghost-button.icon-button:enabled:active - button.fxview-tab-row-main:enabled, - button.fxview-tab-row-main:enabled:hover, - button.fxview-tab-row-main:enabled:active { - background-color: ButtonFace; - } +.fxview-tab-row-button.options-button::part(button) { + background-image: url("chrome://global/skin/icons/more.svg"); } diff --git a/browser/components/firefoxview/helpers.mjs b/browser/components/firefoxview/helpers.mjs index 3cb308a587..b206deef18 100644 --- a/browser/components/firefoxview/helpers.mjs +++ b/browser/components/firefoxview/helpers.mjs @@ -173,3 +173,20 @@ export function escapeHtmlEntities(text) { .replace(/"/g, """) .replace(/'/g, "'"); } + +export function navigateToLink(e) { + let currentWindow = + e.target.ownerGlobal.browsingContext.embedderWindowGlobal.browsingContext + .window; + if (currentWindow.openTrustedLinkIn) { + let where = lazy.BrowserUtils.whereToOpenLink( + e.detail.originalEvent, + false, + true + ); + if (where == "current") { + where = "tab"; + } + currentWindow.openTrustedLinkIn(e.originalTarget.url, where); + } +} diff --git a/browser/components/firefoxview/history.css b/browser/components/firefoxview/history.css index dd2786a8c7..a10291ddb5 100644 --- a/browser/components/firefoxview/history.css +++ b/browser/components/firefoxview/history.css @@ -51,19 +51,8 @@ cursor: pointer; } -.import-history-banner .close { +moz-button.close::part(button) { background-image: url("chrome://global/skin/icons/close-12.svg"); - background-repeat: no-repeat; - background-position: center center; - -moz-context-properties: fill; - fill: currentColor; - min-width: auto; - min-height: auto; - width: 24px; - height: 24px; - margin: 0; - padding: 0; - flex-shrink: 0; } dialog { diff --git a/browser/components/firefoxview/history.mjs b/browser/components/firefoxview/history.mjs index 1fe028449b..478422d49b 100644 --- a/browser/components/firefoxview/history.mjs +++ b/browser/components/firefoxview/history.mjs @@ -7,18 +7,21 @@ import { ifDefined, when, } from "chrome://global/content/vendor/lit.all.mjs"; -import { escapeHtmlEntities, isSearchEnabled } from "./helpers.mjs"; +import { + escapeHtmlEntities, + isSearchEnabled, + navigateToLink, +} from "./helpers.mjs"; import { ViewPage } from "./viewpage.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/migration/migration-wizard.mjs"; +import { HistoryController } from "./HistoryController.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", - FirefoxViewPlacesQuery: - "resource:///modules/firefox-view-places-query.sys.mjs", - PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", }); @@ -26,13 +29,6 @@ let XPCOMUtils = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ).XPCOMUtils; -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "maxRowsPref", - "browser.firefox-view.max-history-rows", - -1 -); - const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart"; const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history"; const IMPORT_HISTORY_DISMISSED_PREF = @@ -44,35 +40,30 @@ class HistoryInView extends ViewPage { constructor() { super(); this._started = false; - this.allHistoryItems = new Map(); - this.historyMapByDate = []; - this.historyMapBySite = []; // Setting maxTabsLength to -1 for no max this.maxTabsLength = -1; - this.placesQuery = new lazy.FirefoxViewPlacesQuery(); - this.searchQuery = ""; - this.searchResults = null; - this.sortOption = "date"; this.profileAge = 8; this.fullyUpdated = false; this.cumulativeSearches = 0; } + controller = new HistoryController(this, { + searchResultsLimit: SEARCH_RESULTS_LIMIT, + }); + start() { if (this._started) { return; } this._started = true; - this.#updateAllHistoryItems(); - this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data)); + this.controller.updateAllHistoryItems(); this.toggleVisibilityInCardContainer(); } async connectedCallback() { super.connectedCallback(); - await this.updateHistoryData(); XPCOMUtils.defineLazyPreferenceGetter( this, "importHistoryDismissedPref", @@ -91,6 +82,7 @@ class HistoryInView extends ViewPage { this.requestUpdate(); } ); + if (!this.importHistoryDismissedPref && !this.hasImportedHistoryPrefs) { let profileAccessor = await lazy.ProfileAge(); let profileCreateTime = await profileAccessor.created; @@ -106,7 +98,6 @@ class HistoryInView extends ViewPage { return; } this._started = false; - this.placesQuery.close(); this.toggleVisibilityInCardContainer(); } @@ -120,32 +111,6 @@ class HistoryInView extends ViewPage { ); } - async #updateAllHistoryItems(allHistoryItems) { - if (allHistoryItems) { - this.allHistoryItems = allHistoryItems; - } else { - await this.updateHistoryData(); - } - this.resetHistoryMaps(); - this.lists.forEach(list => list.requestUpdate()); - await this.#updateSearchResults(); - } - - async #updateSearchResults() { - if (this.searchQuery) { - try { - this.searchResults = await this.placesQuery.searchHistory( - this.searchQuery, - SEARCH_RESULTS_LIMIT - ); - } catch (e) { - // Connection interrupted, ignore. - } - } else { - this.searchResults = null; - } - } - viewVisibleCallback() { this.start(); } @@ -166,14 +131,8 @@ class HistoryInView extends ViewPage { }; static properties = { - ...ViewPage.properties, - allHistoryItems: { type: Map }, - historyMapByDate: { type: Array }, - historyMapBySite: { type: Array }, // Making profileAge a reactive property for testing profileAge: { type: Number }, - searchResults: { type: Array }, - sortOption: { type: String }, }; async getUpdateComplete() { @@ -181,70 +140,8 @@ class HistoryInView extends ViewPage { await Promise.all(Array.from(this.cards).map(card => card.updateComplete)); } - async updateHistoryData() { - this.allHistoryItems = await this.placesQuery.getHistory({ - daysOld: 60, - limit: lazy.maxRowsPref, - sortBy: this.sortOption, - }); - } - - resetHistoryMaps() { - this.historyMapByDate = []; - this.historyMapBySite = []; - } - - createHistoryMaps() { - if (this.sortOption === "date" && !this.historyMapByDate.length) { - const { - visitsFromToday, - visitsFromYesterday, - visitsByDay, - visitsByMonth, - } = this.placesQuery; - - // Add visits from today and yesterday. - if (visitsFromToday.length) { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-today", - items: visitsFromToday, - }); - } - if (visitsFromYesterday.length) { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-yesterday", - items: visitsFromYesterday, - }); - } - - // Add visits from this month, grouped by day. - visitsByDay.forEach(visits => { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-this-month", - items: visits, - }); - }); - - // Add visits from previous months, grouped by month. - visitsByMonth.forEach(visits => { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-prev-month", - items: visits, - }); - }); - } else if (this.sortOption === "site" && !this.historyMapBySite.length) { - this.historyMapBySite = Array.from( - this.allHistoryItems.entries(), - ([domain, items]) => ({ - domain, - items, - l10nId: domain ? null : "firefoxview-history-site-localhost", - }) - ).sort((a, b) => a.domain.localeCompare(b.domain)); - } - } - onPrimaryAction(e) { + navigateToLink(e); // Record telemetry Services.telemetry.recordEvent( "firefoxview_next", @@ -254,26 +151,13 @@ class HistoryInView extends ViewPage { {} ); - if (this.searchQuery) { + if (this.controller.searchQuery) { const searchesHistogram = Services.telemetry.getKeyedHistogramById( "FIREFOX_VIEW_CUMULATIVE_SEARCHES" ); searchesHistogram.add("history", this.cumulativeSearches); this.cumulativeSearches = 0; } - - let currentWindow = this.getWindow(); - if (currentWindow.openTrustedLinkIn) { - let where = lazy.BrowserUtils.whereToOpenLink( - e.detail.originalEvent, - false, - true - ); - if (where == "current") { - where = "tab"; - } - currentWindow.openTrustedLinkIn(e.originalTarget.url, where); - } } onSecondaryAction(e) { @@ -282,24 +166,29 @@ class HistoryInView extends ViewPage { } deleteFromHistory(e) { - lazy.PlacesUtils.history.remove(this.triggerNode.url); + this.controller.deleteFromHistory(); this.recordContextMenuTelemetry("delete-from-history", e); } async onChangeSortOption(e) { - this.sortOption = e.target.value; + await this.controller.onChangeSortOption(e); Services.telemetry.recordEvent( "firefoxview_next", "sort_history", "tabs", null, { - sort_type: this.sortOption, - search_start: this.searchQuery ? "true" : "false", + sort_type: this.controller.sortOption, + search_start: this.controller.searchQuery ? "true" : "false", } ); - await this.updateHistoryData(); - await this.#updateSearchResults(); + } + + async onSearchQuery(e) { + await this.controller.onSearchQuery(e); + this.cumulativeSearches = this.controller.searchQuery + ? this.cumulativeSearches + 1 + : 0; } showAllHistory() { @@ -396,9 +285,9 @@ class HistoryInView extends ViewPage { * The template to use for cards-container. */ get cardsTemplate() { - if (this.searchResults) { + if (this.controller.searchResults) { return this.#searchResultsTemplate(); - } else if (this.allHistoryItems.size) { + } else if (this.controller.allHistoryItems.size) { return this.#historyCardsTemplate(); } return this.#emptyMessageTemplate(); @@ -406,8 +295,11 @@ class HistoryInView extends ViewPage { #historyCardsTemplate() { let cardsTemplate = []; - if (this.sortOption === "date" && this.historyMapByDate.length) { - this.historyMapByDate.forEach(historyItem => { + if ( + this.controller.sortOption === "date" && + this.controller.historyMapByDate.length + ) { + this.controller.historyMapByDate.forEach(historyItem => { if (historyItem.items.length) { let dateArg = JSON.stringify({ date: historyItem.items[0].time }); cardsTemplate.push(html`<card-container> @@ -424,7 +316,7 @@ class HistoryInView extends ViewPage { : "time"} hasPopup="menu" maxTabsLength=${this.maxTabsLength} - .tabItems=${historyItem.items} + .tabItems=${[...historyItem.items]} @fxview-tab-list-primary-action=${this.onPrimaryAction} @fxview-tab-list-secondary-action=${this.onSecondaryAction} > @@ -433,8 +325,8 @@ class HistoryInView extends ViewPage { </card-container>`); } }); - } else if (this.historyMapBySite.length) { - this.historyMapBySite.forEach(historyItem => { + } else if (this.controller.historyMapBySite.length) { + this.controller.historyMapBySite.forEach(historyItem => { if (historyItem.items.length) { cardsTemplate.push(html`<card-container> <h3 slot="header" data-l10n-id="${ifDefined(historyItem.l10nId)}"> @@ -446,7 +338,7 @@ class HistoryInView extends ViewPage { dateTimeFormat="dateTime" hasPopup="menu" maxTabsLength=${this.maxTabsLength} - .tabItems=${historyItem.items} + .tabItems=${[...historyItem.items]} @fxview-tab-list-primary-action=${this.onPrimaryAction} @fxview-tab-list-secondary-action=${this.onSecondaryAction} > @@ -504,17 +396,17 @@ class HistoryInView extends ViewPage { slot="header" data-l10n-id="firefoxview-search-results-header" data-l10n-args=${JSON.stringify({ - query: escapeHtmlEntities(this.searchQuery), + query: escapeHtmlEntities(this.controller.searchQuery), })} ></h3> ${when( - this.searchResults.length, + this.controller.searchResults.length, () => html`<h3 slot="secondary-header" data-l10n-id="firefoxview-search-results-count" data-l10n-args="${JSON.stringify({ - count: this.searchResults.length, + count: this.controller.searchResults.length, })}" ></h3>` )} @@ -524,10 +416,11 @@ class HistoryInView extends ViewPage { dateTimeFormat="dateTime" hasPopup="menu" maxTabsLength="-1" - .searchQuery=${this.searchQuery} - .tabItems=${this.searchResults} + .searchQuery=${this.controller.searchQuery} + .tabItems=${this.controller.searchResults} @fxview-tab-list-primary-action=${this.onPrimaryAction} @fxview-tab-list-secondary-action=${this.onSecondaryAction} + .searchInProgress=${this.controller.placesQuery.searchInProgress} > ${this.panelListTemplate()} </fxview-tab-list> @@ -569,7 +462,7 @@ class HistoryInView extends ViewPage { id="sort-by-date" name="history-sort-option" value="date" - ?checked=${this.sortOption === "date"} + ?checked=${this.controller.sortOption === "date"} @click=${this.onChangeSortOption} /> <label @@ -583,7 +476,7 @@ class HistoryInView extends ViewPage { id="sort-by-site" name="history-sort-option" value="site" - ?checked=${this.sortOption === "site"} + ?checked=${this.controller.sortOption === "site"} @click=${this.onChangeSortOption} /> <label @@ -612,11 +505,12 @@ class HistoryInView extends ViewPage { data-l10n-id="firefoxview-choose-browser-button" @click=${this.openMigrationWizard} ></button> - <button - class="close ghost-button" + <moz-button + class="close" + type="icon ghost" data-l10n-id="firefoxview-import-history-close-button" @click=${this.dismissImportHistory} - ></button> + ></moz-button> </div> </div> </card-container> @@ -624,32 +518,24 @@ class HistoryInView extends ViewPage { </div> <div class="show-all-history-footer" - ?hidden=${!this.allHistoryItems.size} + ?hidden=${!this.controller.allHistoryItems.size} > <button class="show-all-history-button" data-l10n-id="firefoxview-show-all-history" @click=${this.showAllHistory} - ?hidden=${this.searchResults} + ?hidden=${this.controller.searchResults} ></button> </div> `; } - async onSearchQuery(e) { - this.searchQuery = e.detail.query; - this.cumulativeSearches = this.searchQuery - ? this.cumulativeSearches + 1 - : 0; - this.#updateSearchResults(); - } - - willUpdate(changedProperties) { + willUpdate() { this.fullyUpdated = false; - if (this.allHistoryItems.size && !changedProperties.has("sortOption")) { + if (this.controller.allHistoryItems.size) { // onChangeSortOption() will update history data once it has been fetched // from the API. - this.createHistoryMaps(); + this.controller.createHistoryMaps(); } } } diff --git a/browser/components/firefoxview/jar.mn b/browser/components/firefoxview/jar.mn index 1e5cc3e690..8bf3597aa5 100644 --- a/browser/components/firefoxview/jar.mn +++ b/browser/components/firefoxview/jar.mn @@ -9,6 +9,7 @@ browser.jar: content/browser/firefoxview/firefoxview.mjs content/browser/firefoxview/history.css content/browser/firefoxview/history.mjs + content/browser/firefoxview/HistoryController.mjs content/browser/firefoxview/opentabs.mjs content/browser/firefoxview/view-opentabs.css content/browser/firefoxview/syncedtabs.mjs @@ -23,6 +24,9 @@ browser.jar: content/browser/firefoxview/fxview-tab-list.css content/browser/firefoxview/fxview-tab-list.mjs content/browser/firefoxview/fxview-tab-row.css + content/browser/firefoxview/opentabs-tab-list.css + content/browser/firefoxview/opentabs-tab-list.mjs + content/browser/firefoxview/opentabs-tab-row.css content/browser/firefoxview/recentlyclosed.mjs content/browser/firefoxview/viewpage.mjs content/browser/firefoxview/history-empty.svg (content/history-empty.svg) diff --git a/browser/components/firefoxview/opentabs-tab-list.css b/browser/components/firefoxview/opentabs-tab-list.css new file mode 100644 index 0000000000..9245a0fada --- /dev/null +++ b/browser/components/firefoxview/opentabs-tab-list.css @@ -0,0 +1,32 @@ +/* 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/. */ + +.fxview-tab-list { + &.pinned { + display: flex; + flex-wrap: wrap; + + > virtual-list { + display: block; + } + + > opentabs-tab-row { + display: block; + margin-block-end: var(--space-xsmall); + } + } + + &.hasContainerTab { + grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content; + } +} + +virtual-list { + grid-column: span 9; + + .top-padding, + .bottom-padding { + grid-column: span 9; + } +} diff --git a/browser/components/firefoxview/opentabs-tab-list.mjs b/browser/components/firefoxview/opentabs-tab-list.mjs new file mode 100644 index 0000000000..4b6d6b3c86 --- /dev/null +++ b/browser/components/firefoxview/opentabs-tab-list.mjs @@ -0,0 +1,593 @@ +/* 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 { + classMap, + html, + ifDefined, + styleMap, + when, +} from "chrome://global/content/vendor/lit.all.mjs"; +import { + FxviewTabListBase, + FxviewTabRowBase, +} from "chrome://browser/content/firefoxview/fxview-tab-list.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; + +const lazy = {}; +let XPCOMUtils; + +XPCOMUtils = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +).XPCOMUtils; +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "virtualListEnabledPref", + "browser.firefox-view.virtual-list.enabled" +); + +/** + * A list of clickable tab items + * + * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view + */ + +export class OpenTabsTabList extends FxviewTabListBase { + constructor() { + super(); + this.pinnedTabsGridView = false; + this.pinnedTabs = []; + this.unpinnedTabs = []; + } + + static properties = { + pinnedTabsGridView: { type: Boolean }, + }; + + static queries = { + ...FxviewTabListBase.queries, + rowEls: { + all: "opentabs-tab-row", + }, + }; + + willUpdate(changes) { + this.activeIndex = Math.min( + Math.max(this.activeIndex, 0), + this.tabItems.length - 1 + ); + + if (changes.has("dateTimeFormat") || changes.has("updatesPaused")) { + this.clearIntervalTimer(); + if (!this.updatesPaused && this.dateTimeFormat == "relative") { + this.startIntervalTimer(); + this.onIntervalUpdate(); + } + } + + // Move pinned tabs to the beginning of the list + if (this.pinnedTabsGridView) { + // Can set maxTabsLength to -1 to have no max + this.unpinnedTabs = this.tabItems.filter( + tab => !tab.indicators.includes("pinned") + ); + this.pinnedTabs = this.tabItems.filter(tab => + tab.indicators.includes("pinned") + ); + if (this.maxTabsLength > 0) { + this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength); + } + this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs]; + } else if (this.maxTabsLength > 0) { + this.tabItems = this.tabItems.slice(0, this.maxTabsLength); + } + } + + /** + * Focuses the expected element (either the link or button) within fxview-tab-row + * The currently focused/active element ID within a row is stored in this.currentActiveElementId + */ + handleFocusElementInRow(e) { + let fxviewTabRow = e.target; + if (e.code == "ArrowUp") { + // Focus either the link or button of the previous row based on this.currentActiveElementId + e.preventDefault(); + if ( + (this.pinnedTabsGridView && + this.activeIndex >= this.pinnedTabs.length) || + !this.pinnedTabsGridView + ) { + this.focusPrevRow(); + } + } else if (e.code == "ArrowDown") { + // Focus either the link or button of the next row based on this.currentActiveElementId + e.preventDefault(); + if ( + this.pinnedTabsGridView && + this.activeIndex < this.pinnedTabs.length + ) { + this.focusIndex(this.pinnedTabs.length); + } else { + this.focusNextRow(); + } + } else if (e.code == "ArrowRight") { + // Focus either the link or the button in the current row and + // set this.currentActiveElementId to that element's ID + e.preventDefault(); + if (document.dir == "rtl") { + fxviewTabRow.moveFocusLeft(); + } else { + fxviewTabRow.moveFocusRight(); + } + } else if (e.code == "ArrowLeft") { + // Focus either the link or the button in the current row and + // set this.currentActiveElementId to that element's ID + e.preventDefault(); + if (document.dir == "rtl") { + fxviewTabRow.moveFocusRight(); + } else { + fxviewTabRow.moveFocusLeft(); + } + } + } + + async focusIndex(index) { + // Focus link or button of item + if ( + ((this.pinnedTabsGridView && index > this.pinnedTabs.length) || + !this.pinnedTabsGridView) && + lazy.virtualListEnabledPref + ) { + let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length); + if (!row) { + return; + } + let subList = this.rootVirtualListEl.getSubListForItem( + index - this.pinnedTabs.length + ); + if (!subList) { + return; + } + this.activeIndex = index; + + // In Bug 1866845, these manual updates to the sublists should be removed + // and scrollIntoView() should also be iterated on so that we aren't constantly + // moving the focused item to the center of the viewport + for (const sublist of Array.from(this.rootVirtualListEl.children)) { + await sublist.requestUpdate(); + await sublist.updateComplete; + } + row.scrollIntoView({ block: "center" }); + row.focus(); + } else if (index >= 0 && index < this.rowEls?.length) { + this.rowEls[index].focus(); + this.activeIndex = index; + } + } + + #getTabListWrapperClasses() { + let wrapperClasses = ["fxview-tab-list"]; + let tabsToCheck = this.pinnedTabsGridView + ? this.unpinnedTabs + : this.tabItems; + if (tabsToCheck.some(tab => tab.containerObj)) { + wrapperClasses.push(`hasContainerTab`); + } + return wrapperClasses; + } + + itemTemplate = (tabItem, i) => { + let time; + if (tabItem.time || tabItem.closedAt) { + let stringTime = (tabItem.time || tabItem.closedAt).toString(); + // Different APIs return time in different units, so we use + // the length to decide if it's milliseconds or nanoseconds. + if (stringTime.length === 16) { + time = (tabItem.time || tabItem.closedAt) / 1000; + } else { + time = tabItem.time || tabItem.closedAt; + } + } + + return html`<opentabs-tab-row + ?active=${i == this.activeIndex} + class=${classMap({ + pinned: + this.pinnedTabsGridView && tabItem.indicators?.includes("pinned"), + })} + .currentActiveElementId=${this.currentActiveElementId} + .favicon=${tabItem.icon} + .compact=${this.compactRows} + .containerObj=${ifDefined(tabItem.containerObj)} + .indicators=${tabItem.indicators} + .pinnedTabsGridView=${ifDefined(this.pinnedTabsGridView)} + .primaryL10nId=${tabItem.primaryL10nId} + .primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)} + .secondaryL10nId=${tabItem.secondaryL10nId} + .secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)} + .tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)} + .tertiaryL10nArgs=${ifDefined(tabItem.tertiaryL10nArgs)} + .secondaryActionClass=${this.secondaryActionClass} + .tertiaryActionClass=${ifDefined(this.tertiaryActionClass)} + .sourceClosedId=${ifDefined(tabItem.sourceClosedId)} + .sourceWindowId=${ifDefined(tabItem.sourceWindowId)} + .closedId=${ifDefined(tabItem.closedId || tabItem.closedId)} + role=${tabItem.pinned && this.pinnedTabsGridView ? "tab" : "listitem"} + .tabElement=${ifDefined(tabItem.tabElement)} + .time=${ifDefined(time)} + .title=${tabItem.title} + .url=${tabItem.url} + .searchQuery=${ifDefined(this.searchQuery)} + .timeMsPref=${ifDefined(this.timeMsPref)} + .hasPopup=${this.hasPopup} + .dateTimeFormat=${this.dateTimeFormat} + ></opentabs-tab-row>`; + }; + + render() { + if (this.searchQuery && this.tabItems.length === 0) { + return this.emptySearchResultsTemplate(); + } + return html` + ${this.stylesheets()} + <link + rel="stylesheet" + href="chrome://browser/content/firefoxview/opentabs-tab-list.css" + /> + ${when( + this.pinnedTabsGridView && this.pinnedTabs.length, + () => html` + <div + id="fxview-tab-list" + class="fxview-tab-list pinned" + data-l10n-id="firefoxview-pinned-tabs" + role="tablist" + @keydown=${this.handleFocusElementInRow} + > + ${this.pinnedTabs.map((tabItem, i) => + this.customItemTemplate + ? this.customItemTemplate(tabItem, i) + : this.itemTemplate(tabItem, i) + )} + </div> + ` + )} + <div + id="fxview-tab-list" + class=${this.#getTabListWrapperClasses().join(" ")} + data-l10n-id="firefoxview-tabs" + role="list" + @keydown=${this.handleFocusElementInRow} + > + ${when( + lazy.virtualListEnabledPref, + () => html` + <virtual-list + .activeIndex=${this.activeIndex} + .pinnedTabsIndexOffset=${this.pinnedTabsGridView + ? this.pinnedTabs.length + : 0} + .items=${this.pinnedTabsGridView + ? this.unpinnedTabs + : this.tabItems} + .template=${this.itemTemplate} + ></virtual-list> + `, + () => + html`${this.tabItems.map((tabItem, i) => + this.itemTemplate(tabItem, i) + )}` + )} + </div> + <slot name="menu"></slot> + `; + } +} +customElements.define("opentabs-tab-list", OpenTabsTabList); + +/** + * A tab item that displays favicon, title, url, and time of last access + * + * @property {object} containerObj - Info about an open tab's container if within one + * @property {string} indicators - An array of tab indicators if any are present + * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view + */ + +export class OpenTabsTabRow extends FxviewTabRowBase { + constructor() { + super(); + this.indicators = []; + this.pinnedTabsGridView = false; + } + + static properties = { + ...FxviewTabRowBase.properties, + containerObj: { type: Object }, + indicators: { type: Array }, + pinnedTabsGridView: { type: Boolean }, + }; + + static queries = { + ...FxviewTabRowBase.queries, + mediaButtonEl: "#fxview-tab-row-media-button", + pinnedTabButtonEl: "moz-button#fxview-tab-row-main", + }; + + connectedCallback() { + super.connectedCallback(); + this.addEventListener("keydown", this.handleKeydown); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.removeEventListener("keydown", this.handleKeydown); + } + + handleKeydown(e) { + if ( + this.active && + this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + e.key === "m" && + e.ctrlKey + ) { + this.muteOrUnmuteTab(); + } + } + + moveFocusRight() { + let tabList = this.getRootNode().host; + if (this.pinnedTabsGridView && this.indicators?.includes("pinned")) { + tabList.focusNextRow(); + } else if ( + (this.indicators?.includes("soundplaying") || + this.indicators?.includes("muted")) && + this.currentActiveElementId === "fxview-tab-row-main" + ) { + this.focusMediaButton(); + } else if ( + this.currentActiveElementId === "fxview-tab-row-media-button" || + this.currentActiveElementId === "fxview-tab-row-main" + ) { + this.focusSecondaryButton(); + } else if ( + this.tertiaryButtonEl && + this.currentActiveElementId === "fxview-tab-row-secondary-button" + ) { + this.focusTertiaryButton(); + } + } + + moveFocusLeft() { + let tabList = this.getRootNode().host; + if ( + this.pinnedTabsGridView && + (this.indicators?.includes("pinned") || + (tabList.currentActiveElementId === "fxview-tab-row-main" && + tabList.activeIndex === tabList.pinnedTabs.length)) + ) { + tabList.focusPrevRow(); + } else if ( + tabList.currentActiveElementId === "fxview-tab-row-tertiary-button" + ) { + this.focusSecondaryButton(); + } else if ( + (this.indicators?.includes("soundplaying") || + this.indicators?.includes("muted")) && + tabList.currentActiveElementId === "fxview-tab-row-secondary-button" + ) { + this.focusMediaButton(); + } else { + this.focusLink(); + } + } + + focusMediaButton() { + let tabList = this.getRootNode().host; + this.mediaButtonEl.focus(); + tabList.currentActiveElementId = this.mediaButtonEl.id; + } + + #secondaryActionHandler(event) { + if ( + (this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + event.type == "contextmenu") || + (event.type == "click" && event.detail && !event.altKey) || + // detail=0 is from keyboard + (event.type == "click" && !event.detail) + ) { + event.preventDefault(); + this.dispatchEvent( + new CustomEvent("fxview-tab-list-secondary-action", { + bubbles: true, + composed: true, + detail: { originalEvent: event, item: this }, + }) + ); + } + } + + #faviconTemplate() { + return html`<span + class="${classMap({ + "fxview-tab-row-favicon-wrapper": true, + pinned: this.indicators?.includes("pinned"), + pinnedOnNewTab: this.indicators?.includes("pinnedOnNewTab"), + attention: this.indicators?.includes("attention"), + bookmark: this.indicators?.includes("bookmark"), + })}" + > + <span + class="fxview-tab-row-favicon icon" + id="fxview-tab-row-favicon" + style=${styleMap({ + backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`, + })} + ></span> + ${when( + this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + (this.indicators?.includes("muted") || + this.indicators?.includes("soundplaying")), + () => html` + <button + class="fxview-tab-row-pinned-media-button" + id="fxview-tab-row-media-button" + tabindex="-1" + data-l10n-id=${this.indicators?.includes("muted") + ? "fxviewtabrow-unmute-tab-button-no-context" + : "fxviewtabrow-mute-tab-button-no-context"} + muted=${this.indicators?.includes("muted")} + soundplaying=${this.indicators?.includes("soundplaying") && + !this.indicators?.includes("muted")} + @click=${this.muteOrUnmuteTab} + ></button> + ` + )} + </span>`; + } + + #getContainerClasses() { + let containerClasses = ["fxview-tab-row-container-indicator", "icon"]; + if (this.containerObj) { + let { icon, color } = this.containerObj; + containerClasses.push(`identity-icon-${icon}`); + containerClasses.push(`identity-color-${color}`); + } + return containerClasses; + } + + muteOrUnmuteTab(e) { + e?.preventDefault(); + // If the tab has no sound playing, the mute/unmute button will be removed when toggled. + // We should move the focus to the right in that case. This does not apply to pinned tabs + // on the Open Tabs page. + let shouldMoveFocus = + (!this.pinnedTabsGridView || + (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) && + this.mediaButtonEl && + !this.indicators.includes("soundplaying") && + this.currentActiveElementId === "fxview-tab-row-media-button"; + + // detail=0 is from keyboard + if (e?.type == "click" && !e?.detail && shouldMoveFocus) { + if (document.dir == "rtl") { + this.moveFocusLeft(); + } else { + this.moveFocusRight(); + } + } + this.tabElement.toggleMuteAudio(); + } + + #mediaButtonTemplate() { + return html`${when( + this.indicators?.includes("soundplaying") || + this.indicators?.includes("muted"), + () => html`<moz-button + type="icon ghost" + class="fxview-tab-row-button" + id="fxview-tab-row-media-button" + data-l10n-id=${this.indicators?.includes("muted") + ? "fxviewtabrow-unmute-tab-button-no-context" + : "fxviewtabrow-mute-tab-button-no-context"} + muted=${this.indicators?.includes("muted")} + soundplaying=${this.indicators?.includes("soundplaying") && + !this.indicators?.includes("muted")} + @click=${this.muteOrUnmuteTab} + tabindex="${this.active && + this.currentActiveElementId === "fxview-tab-row-media-button" + ? "0" + : "-1"}" + ></moz-button>`, + () => html`<span></span>` + )}`; + } + + #containerIndicatorTemplate() { + let tabList = this.getRootNode().host; + let tabsToCheck = tabList.pinnedTabsGridView + ? tabList.unpinnedTabs + : tabList.tabItems; + return html`${when( + tabsToCheck.some(tab => tab.containerObj), + () => html`<span class=${this.#getContainerClasses().join(" ")}></span>` + )}`; + } + + #pinnedTabItemTemplate() { + return html` + <moz-button + type="icon ghost" + id="fxview-tab-row-main" + aria-haspopup=${ifDefined(this.hasPopup)} + data-l10n-id=${ifDefined(this.primaryL10nId)} + data-l10n-args=${ifDefined(this.primaryL10nArgs)} + tabindex=${this.active && + this.currentActiveElementId === "fxview-tab-row-main" + ? "0" + : "-1"} + role="tab" + @click=${this.primaryActionHandler} + @keydown=${this.primaryActionHandler} + @contextmenu=${this.#secondaryActionHandler} + > + ${this.#faviconTemplate()} + </moz-button> + `; + } + + #unpinnedTabItemTemplate() { + return html`<a + href=${ifDefined(this.url)} + class="fxview-tab-row-main" + id="fxview-tab-row-main" + tabindex=${this.active && + this.currentActiveElementId === "fxview-tab-row-main" + ? "0" + : "-1"} + data-l10n-id=${ifDefined(this.primaryL10nId)} + data-l10n-args=${ifDefined(this.primaryL10nArgs)} + @click=${this.primaryActionHandler} + @keydown=${this.primaryActionHandler} + title=${!this.primaryL10nId ? this.url : null} + > + ${this.#faviconTemplate()} ${this.titleTemplate()} + ${when( + !this.compact, + () => html`${this.#containerIndicatorTemplate()} ${this.urlTemplate()} + ${this.dateTemplate()} ${this.timeTemplate()}` + )} + </a> + ${this.#mediaButtonTemplate()} ${this.secondaryButtonTemplate()} + ${this.tertiaryButtonTemplate()}`; + } + + render() { + return html` + ${this.stylesheets()} + <link + rel="stylesheet" + href="chrome://browser/content/firefoxview/opentabs-tab-row.css" + /> + ${when( + this.containerObj, + () => html` + <link + rel="stylesheet" + href="chrome://browser/content/usercontext/usercontext.css" + /> + ` + )} + ${when( + this.pinnedTabsGridView && this.indicators?.includes("pinned"), + this.#pinnedTabItemTemplate.bind(this), + this.#unpinnedTabItemTemplate.bind(this) + )} + `; + } +} +customElements.define("opentabs-tab-row", OpenTabsTabRow); diff --git a/browser/components/firefoxview/opentabs-tab-row.css b/browser/components/firefoxview/opentabs-tab-row.css new file mode 100644 index 0000000000..e5c00884b3 --- /dev/null +++ b/browser/components/firefoxview/opentabs-tab-row.css @@ -0,0 +1,119 @@ +/* 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/. */ + +.fxview-tab-row-favicon-wrapper { + height: 16px; + position: relative; + display: block; + + .fxview-tab-row-favicon::after, + .fxview-tab-row-button::after, + &.pinned .fxview-tab-row-pinned-media-button { + display: block; + content: ""; + background-size: 12px; + background-position: center; + background-repeat: no-repeat; + position: relative; + height: 12px; + width: 12px; + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: var(--fxview-background-color-secondary); + } + + &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after { + inset-block-start: 9px; + inset-inline-end: -6px; + } + + &.pinnedOnNewTab .fxview-tab-row-favicon::after, + &.pinnedOnNewTab .fxview-tab-row-button::after { + background-image: url("chrome://browser/skin/pin-12.svg"); + } + + &.bookmark .fxview-tab-row-favicon::after, + &.bookmark .fxview-tab-row-button::after { + background-image: url("chrome://browser/skin/bookmark-12.svg"); + fill: var(--fxview-primary-action-background); + } + + &.attention .fxview-tab-row-favicon::after, + &.attention .fxview-tab-row-button::after { + background-image: radial-gradient(circle, var(--attention-dot-color), var(--attention-dot-color) 2px, transparent 2px); + height: 4px; + width: 100%; + inset-block-start: 20px; + } + + &.pinned .fxview-tab-row-pinned-media-button { + inset-block-start: -5px; + inset-inline-end: 1px; + border: var(--button-border); + border-radius: 100%; + background-color: var(--fxview-background-color-secondary); + padding: 6px; + min-width: 0; + min-height: 0; + position: absolute; + + &[muted="true"] { + background-image: url("chrome://global/skin/media/audio-muted.svg"); + } + + &[soundplaying="true"] { + background-image: url("chrome://global/skin/media/audio.svg"); + } + + &:active, + &:hover:active { + background-color: var(--button-background-color-active); + } + } +} + +.fxview-tab-row-container-indicator { + height: 16px; + width: 16px; + background-image: var(--identity-icon); + background-size: cover; + -moz-context-properties: fill; + fill: var(--identity-icon-color); +} + +.fxview-tab-row-main { + :host(.pinned) & { + padding: var(--space-small); + min-width: unset; + margin: 0; + } +} + +button.fxview-tab-row-main:hover { + & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after { + stroke: var(--fxview-indicator-stroke-color-hover); + } +} + +@media (prefers-contrast) { + button.fxview-tab-row-main { + border: 1px solid ButtonText; + color: ButtonText; + } + + button.fxview-tab-row-main:enabled:hover { + border: 1px solid SelectedItem; + color: SelectedItem; + } + + button.fxview-tab-row-main:enabled:active { + color: SelectedItem; + } + + button.fxview-tab-row-main:enabled, + button.fxview-tab-row-main:enabled:hover, + button.fxview-tab-row-main:enabled:active { + background-color: ButtonFace; + } +} diff --git a/browser/components/firefoxview/opentabs.mjs b/browser/components/firefoxview/opentabs.mjs index 8d7723e931..fb84553e26 100644 --- a/browser/components/firefoxview/opentabs.mjs +++ b/browser/components/firefoxview/opentabs.mjs @@ -17,6 +17,8 @@ import { MAX_TABS_FOR_RECENT_BROWSING, } from "./helpers.mjs"; import { ViewPage, ViewPageContent } from "./viewpage.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/firefoxview/opentabs-tab-list.mjs"; const lazy = {}; @@ -36,6 +38,9 @@ ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { ).getFxAccountsSingleton(); }); +const TOPIC_DEVICESTATE_CHANGED = "firefox-view.devicestate.changed"; +const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated"; + /** * A collection of open tabs grouped by window. * @@ -339,7 +344,7 @@ class OpenTabsInView extends ViewPage { ></view-opentabs-card>`; } - handleEvent({ detail, target, type }) { + handleEvent({ detail, type }) { if (this.recentBrowsing && type === "fxview-search-textbox-query") { this.onSearchQuery({ detail }); return; @@ -424,7 +429,7 @@ class OpenTabsInViewCard extends ViewPageContent { static queries = { cardEl: "card-container", tabContextMenu: "view-opentabs-contextmenu", - tabList: "fxview-tab-list", + tabList: "opentabs-tab-list", }; openContextMenu(e) { @@ -565,7 +570,7 @@ class OpenTabsInViewCard extends ViewPageContent { () => html`<h3 slot="header">${this.title}</h3>` )} <div class="fxview-tab-list-container" slot="main"> - <fxview-tab-list + <opentabs-tab-list .hasPopup=${"menu"} ?compactRows=${this.classList.contains("width-limited")} @fxview-tab-list-primary-action=${this.onTabListRowClick} @@ -579,7 +584,7 @@ class OpenTabsInViewCard extends ViewPageContent { .searchQuery=${this.searchQuery} .pinnedTabsGridView=${!this.recentBrowsing} ><view-opentabs-contextmenu slot="menu"></view-opentabs-contextmenu> - </fxview-tab-list> + </opentabs-tab-list> </div> ${when( this.recentBrowsing, @@ -659,7 +664,7 @@ customElements.define("view-opentabs-card", OpenTabsInViewCard); class OpenTabsContextMenu extends MozLitElement { static properties = { devices: { type: Array }, - triggerNode: { type: Object }, + triggerNode: { hasChanged: () => true, type: Object }, }; static queries = { @@ -669,6 +674,7 @@ class OpenTabsContextMenu extends MozLitElement { constructor() { super(); this.triggerNode = null; + this.boundObserve = (...args) => this.observe(...args); this.devices = []; } @@ -680,6 +686,28 @@ class OpenTabsContextMenu extends MozLitElement { return this.ownerDocument.querySelector("view-opentabs"); } + connectedCallback() { + super.connectedCallback(); + this.fetchDevicesPromise = this.fetchDevices(); + Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); + Services.obs.addObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED); + } + + disconnectedCallback() { + super.disconnectedCallback(); + Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); + Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED); + } + + observe(_subject, topic, _data) { + if ( + topic == TOPIC_DEVICELIST_UPDATED || + topic == TOPIC_DEVICESTATE_CHANGED + ) { + this.fetchDevicesPromise = this.fetchDevices(); + } + } + async fetchDevices() { const currentWindow = this.ownerViewPage.getWindow(); if (currentWindow?.gSync) { @@ -699,7 +727,7 @@ class OpenTabsContextMenu extends MozLitElement { return; } this.triggerNode = triggerNode; - await this.fetchDevices(); + await this.fetchDevicesPromise; await this.getUpdateComplete(); this.panelList.toggle(originalEvent); } @@ -1022,7 +1050,7 @@ function getTabListItems(tabs, isRecentBrowsing) { ? JSON.stringify({ tabTitle: tab.label }) : null, tabElement: tab, - time: tab.lastAccessed, + time: tab.lastSeenActive, title: tab.label, url, }; diff --git a/browser/components/firefoxview/recentlyclosed.mjs b/browser/components/firefoxview/recentlyclosed.mjs index 83c323256c..7efd8d09f2 100644 --- a/browser/components/firefoxview/recentlyclosed.mjs +++ b/browser/components/firefoxview/recentlyclosed.mjs @@ -65,7 +65,7 @@ class RecentlyClosedTabsInView extends ViewPage { tabList: "fxview-tab-list", }; - observe(subject, topic, data) { + observe(subject, topic) { if ( topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED || (topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH && @@ -249,13 +249,22 @@ class RecentlyClosedTabsInView extends ViewPage { onDismissTab(e) { const closedId = parseInt(e.originalTarget.closedId, 10); const sourceClosedId = parseInt(e.originalTarget.sourceClosedId, 10); - const sourceWindowId = e.originalTarget.souceWindowId; - if (sourceWindowId || !isNaN(sourceClosedId)) { + const sourceWindowId = e.originalTarget.sourceWindowId; + if (!isNaN(sourceClosedId)) { + // the sourceClosedId is an identifier for a now-closed window the tab + // was closed in. lazy.SessionStore.forgetClosedTabById(closedId, { sourceClosedId, + }); + } else if (sourceWindowId) { + // the sourceWindowId is an identifier for a currently-open window the tab + // was closed in. + lazy.SessionStore.forgetClosedTabById(closedId, { sourceWindowId, }); } else { + // without either identifier, SessionStore will need to walk its window collections + // to find the close tab with matching closedId lazy.SessionStore.forgetClosedTabById(closedId); } @@ -387,7 +396,6 @@ class RecentlyClosedTabsInView extends ViewPage { () => html` <fxview-tab-list - class="with-dismiss-button" slot="main" .maxTabsLength=${!this.recentBrowsing || this.showAll ? -1 diff --git a/browser/components/firefoxview/syncedtabs.mjs b/browser/components/firefoxview/syncedtabs.mjs index d64da45a30..1c65650c10 100644 --- a/browser/components/firefoxview/syncedtabs.mjs +++ b/browser/components/firefoxview/syncedtabs.mjs @@ -4,13 +4,9 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", - SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", + SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs", }); -const { SyncedTabsErrorHandler } = ChromeUtils.importESModule( - "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs" -); const { TabsSetupFlowManager } = ChromeUtils.importESModule( "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs" ); @@ -24,43 +20,52 @@ import { ViewPage } from "./viewpage.mjs"; import { escapeHtmlEntities, isSearchEnabled, - searchTabList, MAX_TABS_FOR_RECENT_BROWSING, + navigateToLink, } from "./helpers.mjs"; -const SYNCED_TABS_CHANGED = "services.sync.tabs.changed"; -const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed"; const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open"; class SyncedTabsInView extends ViewPage { + controller = new lazy.SyncedTabsController(this, { + contextMenu: true, + pairDeviceCallback: () => + Services.telemetry.recordEvent( + "firefoxview_next", + "fxa_mobile", + "sync", + null, + { + has_devices: TabsSetupFlowManager.secondaryDeviceConnected.toString(), + } + ), + signupCallback: () => + Services.telemetry.recordEvent( + "firefoxview_next", + "fxa_continue", + "sync", + null + ), + }); + constructor() { super(); this._started = false; - this.boundObserve = (...args) => this.observe(...args); - this._currentSetupStateIndex = -1; - this.errorState = null; this._id = Math.floor(Math.random() * 10e6); - this.currentSyncedTabs = []; if (this.recentBrowsing) { this.maxTabsLength = MAX_TABS_FOR_RECENT_BROWSING; } else { // Setting maxTabsLength to -1 for no max this.maxTabsLength = -1; } - this.devices = []; this.fullyUpdated = false; - this.searchQuery = ""; this.showAll = false; this.cumulativeSearches = 0; + this.onSearchQuery = this.onSearchQuery.bind(this); } static properties = { ...ViewPage.properties, - errorState: { type: Number }, - currentSyncedTabs: { type: Array }, - _currentSetupStateIndex: { type: Number }, - devices: { type: Array }, - searchQuery: { type: String }, showAll: { type: Boolean }, cumulativeSearches: { type: Number }, }; @@ -72,26 +77,19 @@ class SyncedTabsInView extends ViewPage { tabLists: { all: "fxview-tab-list" }, }; - connectedCallback() { - super.connectedCallback(); - this.addEventListener("click", this); - } - start() { if (this._started) { return; } this._started = true; - Services.obs.addObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED); - Services.obs.addObserver(this.boundObserve, SYNCED_TABS_CHANGED); - - this.updateStates(); + this.controller.addSyncObservers(); + this.controller.updateStates(); this.onVisibilityChange(); if (this.recentBrowsing) { this.recentBrowsingElement.addEventListener( "fxview-search-textbox-query", - this + this.onSearchQuery ); } } @@ -103,75 +101,21 @@ class SyncedTabsInView extends ViewPage { this._started = false; TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded"); this.onVisibilityChange(); - - Services.obs.removeObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED); - Services.obs.removeObserver(this.boundObserve, SYNCED_TABS_CHANGED); + this.controller.removeSyncObservers(); if (this.recentBrowsing) { this.recentBrowsingElement.removeEventListener( "fxview-search-textbox-query", - this + this.onSearchQuery ); } } - willUpdate(changedProperties) { - if (changedProperties.has("searchQuery")) { - this.cumulativeSearches = this.searchQuery - ? this.cumulativeSearches + 1 - : 0; - } - } - disconnectedCallback() { super.disconnectedCallback(); this.stop(); } - handleEvent(event) { - if (event.type == "click" && event.target.dataset.action) { - const { ErrorType } = SyncedTabsErrorHandler; - switch (event.target.dataset.action) { - case `${ErrorType.SYNC_ERROR}`: - case `${ErrorType.NETWORK_OFFLINE}`: - case `${ErrorType.PASSWORD_LOCKED}`: { - TabsSetupFlowManager.tryToClearError(); - break; - } - case `${ErrorType.SIGNED_OUT}`: - case "sign-in": { - TabsSetupFlowManager.openFxASignup(event.target.ownerGlobal); - break; - } - case "add-device": { - TabsSetupFlowManager.openFxAPairDevice(event.target.ownerGlobal); - break; - } - case "sync-tabs-disabled": { - TabsSetupFlowManager.syncOpenTabs(event.target); - break; - } - case `${ErrorType.SYNC_DISCONNECTED}`: { - const win = event.target.ownerGlobal; - const { switchToTabHavingURI } = - win.docShell.chromeEventHandler.ownerGlobal; - switchToTabHavingURI( - "about:preferences?action=choose-what-to-sync#sync", - true, - {} - ); - break; - } - } - } - if (event.type == "change") { - TabsSetupFlowManager.syncOpenTabs(event.target); - } - if (this.recentBrowsing && event.type === "fxview-search-textbox-query") { - this.onSearchQuery(event); - } - } - viewVisibleCallback() { this.start(); } @@ -196,90 +140,16 @@ class SyncedTabsInView extends ViewPage { this.toggleVisibilityInCardContainer(); } - async observe(subject, topic, errorState) { - if (topic == TOPIC_SETUPSTATE_CHANGED) { - this.updateStates(errorState); - } - if (topic == SYNCED_TABS_CHANGED) { - this.getSyncedTabData(); - } - } - - updateStates(errorState) { - let stateIndex = TabsSetupFlowManager.uiStateIndex; - errorState = errorState || SyncedTabsErrorHandler.getErrorType(); - - if (stateIndex == 4 && this._currentSetupStateIndex !== stateIndex) { - // trigger an initial request for the synced tabs list - this.getSyncedTabData(); - } - - this._currentSetupStateIndex = stateIndex; - this.errorState = errorState; - } - - actionMappings = { - "sign-in": { - header: "firefoxview-syncedtabs-signin-header", - description: "firefoxview-syncedtabs-signin-description", - buttonLabel: "firefoxview-syncedtabs-signin-primarybutton", - }, - "add-device": { - header: "firefoxview-syncedtabs-adddevice-header", - description: "firefoxview-syncedtabs-adddevice-description", - buttonLabel: "firefoxview-syncedtabs-adddevice-primarybutton", - descriptionLink: { - name: "url", - url: "https://support.mozilla.org/kb/how-do-i-set-sync-my-computer#w_connect-additional-devices-to-sync", - }, - }, - "sync-tabs-disabled": { - header: "firefoxview-syncedtabs-synctabs-header", - description: "firefoxview-syncedtabs-synctabs-description", - buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton", - }, - loading: { - header: "firefoxview-syncedtabs-loading-header", - description: "firefoxview-syncedtabs-loading-description", - }, - }; - - generateMessageCard({ error = false, action, errorState }) { - errorState = errorState || this.errorState; - let header, - description, - descriptionLink, - buttonLabel, - headerIconUrl, - mainImageUrl; - let descriptionArray; - if (error) { - let link; - ({ header, description, link, buttonLabel } = - SyncedTabsErrorHandler.getFluentStringsForErrorType(errorState)); - action = `${errorState}`; - headerIconUrl = "chrome://global/skin/icons/info-filled.svg"; - mainImageUrl = - "chrome://browser/content/firefoxview/synced-tabs-error.svg"; - descriptionArray = [description]; - if (errorState == "password-locked") { - descriptionLink = {}; - // This is ugly, but we need to special case this link so we can - // coexist with the old view. - descriptionArray.push("firefoxview-syncedtab-password-locked-link"); - descriptionLink.name = "syncedtab-password-locked-link"; - descriptionLink.url = link.href; - } - } else { - header = this.actionMappings[action].header; - description = this.actionMappings[action].description; - buttonLabel = this.actionMappings[action].buttonLabel; - descriptionLink = this.actionMappings[action].descriptionLink; - mainImageUrl = - "chrome://browser/content/firefoxview/synced-tabs-error.svg"; - descriptionArray = [description]; - } - + generateMessageCard({ + action, + buttonLabel, + descriptionArray, + descriptionLink, + error, + header, + headerIconUrl, + mainImageUrl, + }) { return html` <fxview-empty-state headerLabel=${header} @@ -299,7 +169,7 @@ class SyncedTabsInView extends ViewPage { ?hidden=${!buttonLabel} data-l10n-id="${ifDefined(buttonLabel)}" data-action="${action}" - @click=${this.handleEvent} + @click=${e => this.controller.handleEvent(e)} aria-details="empty-container" ></button> </fxview-empty-state> @@ -307,28 +177,19 @@ class SyncedTabsInView extends ViewPage { } onOpenLink(event) { - let currentWindow = this.getWindow(); - if (currentWindow.openTrustedLinkIn) { - let where = lazy.BrowserUtils.whereToOpenLink( - event.detail.originalEvent, - false, - true - ); - if (where == "current") { - where = "tab"; + navigateToLink(event); + + Services.telemetry.recordEvent( + "firefoxview_next", + "synced_tabs", + "tabs", + null, + { + page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs", } - currentWindow.openTrustedLinkIn(event.originalTarget.url, where); - Services.telemetry.recordEvent( - "firefoxview_next", - "synced_tabs", - "tabs", - null, - { - page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs", - } - ); - } - if (this.searchQuery) { + ); + + if (this.controller.searchQuery) { const searchesHistogram = Services.telemetry.getKeyedHistogramById( "FIREFOX_VIEW_CUMULATIVE_SEARCHES" ); @@ -384,7 +245,7 @@ class SyncedTabsInView extends ViewPage { class="blackbox notabs search-results-empty" data-l10n-id="firefoxview-search-results-empty" data-l10n-args=${JSON.stringify({ - query: escapeHtmlEntities(this.searchQuery), + query: escapeHtmlEntities(this.controller.searchQuery), })} ></div> `, @@ -405,7 +266,8 @@ class SyncedTabsInView extends ViewPage { } onSearchQuery(e) { - this.searchQuery = e.detail.query; + this.controller.searchQuery = e.detail.query; + this.cumulativeSearches = e.detail.query ? this.cumulativeSearches + 1 : 0; this.showAll = false; } @@ -422,7 +284,7 @@ class SyncedTabsInView extends ViewPage { secondaryActionClass="options-button" hasPopup="menu" .tabItems=${ifDefined(tabItems)} - .searchQuery=${this.searchQuery} + .searchQuery=${this.controller.searchQuery} maxTabsLength=${this.showAll ? -1 : this.maxTabsLength} @fxview-tab-list-primary-action=${this.onOpenLink} @fxview-tab-list-secondary-action=${this.onContextMenu} @@ -434,33 +296,9 @@ class SyncedTabsInView extends ViewPage { generateTabList() { let renderArray = []; - let renderInfo = {}; - for (let tab of this.currentSyncedTabs) { - if (!(tab.client in renderInfo)) { - renderInfo[tab.client] = { - name: tab.device, - deviceType: tab.deviceType, - tabs: [], - }; - } - renderInfo[tab.client].tabs.push(tab); - } - - // Add devices without tabs - for (let device of this.devices) { - if (!(device.id in renderInfo)) { - renderInfo[device.id] = { - name: device.name, - deviceType: device.clientType, - tabs: [], - }; - } - } - + let renderInfo = this.controller.getRenderInfo(); for (let id in renderInfo) { - let tabItems = this.searchQuery - ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs)) - : this.getTabItems(renderInfo[id].tabs); + let tabItems = renderInfo[id].tabItems; if (tabItems.length) { const template = this.recentBrowsing ? this.deviceTemplate( @@ -509,7 +347,7 @@ class SyncedTabsInView extends ViewPage { isShowAllLinkVisible(tabItems) { return ( this.recentBrowsing && - this.searchQuery && + this.controller.searchQuery && tabItems.length > this.maxTabsLength && !this.showAll ); @@ -536,35 +374,10 @@ class SyncedTabsInView extends ViewPage { } generateCardContent() { - switch (this._currentSetupStateIndex) { - case 0 /* error-state */: - if (this.errorState) { - return this.generateMessageCard({ error: true }); - } - return this.generateMessageCard({ action: "loading" }); - case 1 /* not-signed-in */: - if (Services.prefs.prefHasUserValue("services.sync.lastversion")) { - // If this pref is set, the user has signed out of sync. - // This path is also taken if we are disconnected from sync. See bug 1784055 - return this.generateMessageCard({ - error: true, - errorState: "signed-out", - }); - } - return this.generateMessageCard({ action: "sign-in" }); - case 2 /* connect-secondary-device*/: - return this.generateMessageCard({ action: "add-device" }); - case 3 /* disabled-tab-sync */: - return this.generateMessageCard({ action: "sync-tabs-disabled" }); - case 4 /* synced-tabs-loaded*/: - // There seems to be an edge case where sync says everything worked - // fine but we have no devices. - if (!this.devices.length) { - return this.generateMessageCard({ action: "add-device" }); - } - return this.generateTabList(); - } - return html``; + const cardProperties = this.controller.getMessageCard(); + return cardProperties + ? this.generateMessageCard(cardProperties) + : this.generateTabList(); } render() { @@ -589,7 +402,7 @@ class SyncedTabsInView extends ViewPage { data-l10n-id="firefoxview-synced-tabs-header" ></h2> ${when( - isSearchEnabled() || this._currentSetupStateIndex === 4, + isSearchEnabled() || this.controller.currentSetupStateIndex === 4, () => html`<div class="syncedtabs-header"> ${when( isSearchEnabled(), @@ -606,12 +419,12 @@ class SyncedTabsInView extends ViewPage { </div>` )} ${when( - this._currentSetupStateIndex === 4, + this.controller.currentSetupStateIndex === 4, () => html` <button class="small-button" data-action="add-device" - @click=${this.handleEvent} + @click=${e => this.controller.handleEvent(e)} > <img class="icon" @@ -635,9 +448,9 @@ class SyncedTabsInView extends ViewPage { html`<card-container preserveCollapseState shortPageName="syncedtabs" - ?showViewAll=${this._currentSetupStateIndex == 4 && - this.currentSyncedTabs.length} - ?isEmptyState=${!this.currentSyncedTabs.length} + ?showViewAll=${this.controller.currentSetupStateIndex == 4 && + this.controller.currentSyncedTabs.length} + ?isEmptyState=${!this.controller.currentSyncedTabs.length} > > <h3 @@ -656,71 +469,9 @@ class SyncedTabsInView extends ViewPage { return renderArray; } - async onReload() { - await TabsSetupFlowManager.syncOnPageReload(); - } - - getTabItems(tabs) { - tabs = tabs || this.tabs; - return tabs?.map(tab => ({ - icon: tab.icon, - title: tab.title, - time: tab.lastUsed * 1000, - url: tab.url, - primaryL10nId: "firefoxview-tabs-list-tab-button", - primaryL10nArgs: JSON.stringify({ targetURI: tab.url }), - secondaryL10nId: "fxviewtabrow-options-menu-button", - secondaryL10nArgs: JSON.stringify({ tabTitle: tab.title }), - })); - } - - updateTabsList(syncedTabs) { - if (!syncedTabs.length) { - this.currentSyncedTabs = syncedTabs; - this.sendTabTelemetry(0); - } - - const tabsToRender = syncedTabs; - - // Return early if new tabs are the same as previous ones - if ( - JSON.stringify(tabsToRender) == JSON.stringify(this.currentSyncedTabs) - ) { - return; - } - - this.currentSyncedTabs = tabsToRender; - // Record the full tab count - this.sendTabTelemetry(syncedTabs.length); - } - - async getSyncedTabData() { - this.devices = await lazy.SyncedTabs.getTabClients(); - let tabs = await lazy.SyncedTabs.createRecentTabsList(this.devices, 50, { - removeAllDupes: false, - removeDeviceDupes: true, - }); - - this.updateTabsList(tabs); - } - updated() { this.fullyUpdated = true; this.toggleVisibilityInCardContainer(); } - - sendTabTelemetry(numTabs) { - /* - Services.telemetry.recordEvent( - "firefoxview_next", - "synced_tabs", - "tabs", - null, - { - count: numTabs.toString(), - } - ); -*/ - } } customElements.define("view-syncedtabs", SyncedTabsInView); diff --git a/browser/components/firefoxview/tests/browser/browser.toml b/browser/components/firefoxview/tests/browser/browser.toml index 9f9c1c0176..db8b2ea25c 100644 --- a/browser/components/firefoxview/tests/browser/browser.toml +++ b/browser/components/firefoxview/tests/browser/browser.toml @@ -27,6 +27,8 @@ skip-if = ["true"] # Bug 1869605 and # Bug 1870296 ["browser_firefoxview.js"] +["browser_firefoxview_dragDrop_pinned_tab.js"] + ["browser_firefoxview_general_telemetry.js"] ["browser_firefoxview_navigation.js"] @@ -51,17 +53,15 @@ skip-if = ["true"] # Bug 1851453 ["browser_opentabs_firefoxview.js"] ["browser_opentabs_more.js"] -fail-if = ["a11y_checks"] # Bugs 1858041, 1854625, and 1872174 clicked Show all link is not accessible because it is "hidden" when clicked skip-if = ["verify"] # Bug 1886017 ["browser_opentabs_pinned_tabs.js"] ["browser_opentabs_recency.js"] skip-if = [ - "os == 'win'", - "os == 'mac' && verify", + "os == 'mac'", "os == 'linux'" -] # macos times out, see bug 1857293, skipped for windows, see bug 1858460, Bug 1875877 - frequent fails on linux. +] # macos times out, see bug 1857293, Bug 1875877 - frequent fails on linux. ["browser_opentabs_search.js"] fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js new file mode 100644 index 0000000000..dd30d53030 --- /dev/null +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function dragAndDrop( + tab1, + tab2, + initialWindow = window, + destWindow = window, + afterTab = true, + context +) { + let rect = tab2.getBoundingClientRect(); + let event = { + ctrlKey: false, + altKey: false, + clientX: rect.left + rect.width / 2 + 10 * (afterTab ? 1 : -1), + clientY: rect.top + rect.height / 2, + }; + + if (destWindow != initialWindow) { + // Make sure that both tab1 and tab2 are visible + initialWindow.focus(); + initialWindow.moveTo(rect.left, rect.top + rect.height * 3); + } + + EventUtils.synthesizeDrop( + tab1, + tab2, + null, + "move", + initialWindow, + destWindow, + event + ); + + // Ensure dnd suppression is cleared. + EventUtils.synthesizeMouseAtCenter(tab2, { type: "mouseup" }, context); +} + +add_task(async function () { + await BrowserTestUtils.openNewForegroundTab(gBrowser, URLs[0]); + await BrowserTestUtils.openNewForegroundTab(gBrowser, URLs[1]); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + let win1 = browser.ownerGlobal; + await navigateToViewAndWait(document, "opentabs"); + + let openTabs = document.querySelector("view-opentabs[name=opentabs]"); + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards[0].tabList.rowEls.length + ); + await openTabs.openTabsTarget.readyWindowsPromise; + let card = openTabs.viewCards[0]; + let tabRows = card.tabList.rowEls; + let tabChangeRaised; + + // Pin first two tabs + for (var i = 0; i < 2; i++) { + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabChange" + ); + let currentTabEl = tabRows[i]; + let currentTab = currentTabEl.tabElement; + info(`Pinning tab ${i + 1} with label: ${currentTab.label}`); + win1.gBrowser.pinTab(currentTab); + await tabChangeRaised; + await openTabs.updateComplete; + tabRows = card.tabList.rowEls; + currentTabEl = tabRows[i]; + + await TestUtils.waitForCondition( + () => currentTabEl.indicators.includes("pinned"), + `Tab ${i + 1} is pinned.` + ); + } + + info(`First two tabs are pinned.`); + + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards.length === 2, + "Two windows are shown for Open Tabs in in Fx View." + ); + + let pinnedTab = win1.gBrowser.visibleTabs[0]; + let newWindowTab = win2.gBrowser.visibleTabs[0]; + + dragAndDrop(newWindowTab, pinnedTab, win2, win1, true, content); + + await switchToFxViewTab(); + await openTabs.updateComplete; + await TestUtils.waitForCondition( + () => openTabs.viewCards.length === 1, + "One window is shown for Open Tabs in in Fx View." + ); + }); + cleanupTabs(); +}); diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js index e61b48b472..52dfce962d 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js @@ -191,42 +191,6 @@ async function checkFxRenderCalls(browser, elements, selectedView) { sandbox.restore(); } -function dragAndDrop( - tab1, - tab2, - initialWindow = window, - destWindow = window, - afterTab = true, - context -) { - let rect = tab2.getBoundingClientRect(); - let event = { - ctrlKey: false, - altKey: false, - clientX: rect.left + rect.width / 2 + 10 * (afterTab ? 1 : -1), - clientY: rect.top + rect.height / 2, - }; - - if (destWindow != initialWindow) { - // Make sure that both tab1 and tab2 are visible - initialWindow.focus(); - initialWindow.moveTo(rect.left, rect.top + rect.height * 3); - } - - EventUtils.synthesizeDrop( - tab1, - tab2, - null, - "move", - initialWindow, - destWindow, - event - ); - - // Ensure dnd suppression is cleared. - EventUtils.synthesizeMouseAtCenter(tab2, { type: "mouseup" }, context); -} - add_task(async function test_recentbrowsing() { await setupOpenAndClosedTabs(); @@ -438,66 +402,3 @@ add_task(async function test_recentlyclosed() { }); await BrowserTestUtils.removeTab(TestTabs.tab2); }); - -add_task(async function test_drag_drop_pinned_tab() { - await setupOpenAndClosedTabs(); - await withFirefoxView({}, async browser => { - const { document } = browser.contentWindow; - let win1 = browser.ownerGlobal; - await navigateToViewAndWait(document, "opentabs"); - - let openTabs = document.querySelector("view-opentabs[name=opentabs]"); - await openTabs.updateComplete; - await TestUtils.waitForCondition( - () => openTabs.viewCards[0].tabList.rowEls.length - ); - await openTabs.openTabsTarget.readyWindowsPromise; - let card = openTabs.viewCards[0]; - let tabRows = card.tabList.rowEls; - let tabChangeRaised; - - // Pin first two tabs - for (var i = 0; i < 2; i++) { - tabChangeRaised = BrowserTestUtils.waitForEvent( - NonPrivateTabs, - "TabChange" - ); - let currentTabEl = tabRows[i]; - let currentTab = currentTabEl.tabElement; - info(`Pinning tab ${i + 1} with label: ${currentTab.label}`); - win1.gBrowser.pinTab(currentTab); - await tabChangeRaised; - await openTabs.updateComplete; - tabRows = card.tabList.rowEls; - currentTabEl = tabRows[i]; - - await TestUtils.waitForCondition( - () => currentTabEl.indicators.includes("pinned"), - `Tab ${i + 1} is pinned.` - ); - } - - info(`First two tabs are pinned.`); - - let win2 = await BrowserTestUtils.openNewBrowserWindow(); - - await openTabs.updateComplete; - await TestUtils.waitForCondition( - () => openTabs.viewCards.length === 2, - "Two windows are shown for Open Tabs in in Fx View." - ); - - let pinnedTab = win1.gBrowser.visibleTabs[0]; - let newWindowTab = win2.gBrowser.visibleTabs[0]; - - dragAndDrop(newWindowTab, pinnedTab, win2, win1, true, content); - - await switchToFxViewTab(); - await openTabs.updateComplete; - await TestUtils.waitForCondition( - () => openTabs.viewCards.length === 1, - "One window is shown for Open Tabs in in Fx View." - ); - }); - cleanupTabs(); -}); diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js index c76a11d3ad..e1aa58ae49 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js @@ -537,7 +537,7 @@ add_task(async function test_cumulative_searches_history_telemetry() { () => history.fullyUpdated && history?.lists[0].rowEls?.length === 1 && - history?.searchQuery, + history?.controller?.searchQuery, "Expected search results are not shown yet." ); @@ -605,7 +605,8 @@ add_task(async function test_cumulative_searches_syncedtabs_telemetry() { ); await TestUtils.waitForCondition( () => - syncedTabs.tabLists[0].rowEls.length === 1 && syncedTabs?.searchQuery, + syncedTabs.tabLists[0].rowEls.length === 1 && + syncedTabs.controller.searchQuery, "Expected search results are not shown yet." ); diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js index 037729ea7d..b556649d52 100644 --- a/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js +++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js @@ -78,7 +78,7 @@ add_task(async function aria_attributes() { "true", 'Firefox View button should have `aria-pressed="true"` upon selecting it' ); - win.BrowserOpenTab(); + win.BrowserCommands.openTab(); is( win.FirefoxViewHandler.button.getAttribute("aria-pressed"), "false", @@ -118,8 +118,8 @@ add_task(async function homepage_new_tab() { win.gBrowser.tabContainer, "TabOpen" ); - win.BrowserHome(); - info("Waiting for BrowserHome() to open a new tab"); + win.BrowserCommands.home(); + info("Waiting for BrowserCommands.home() to open a new tab"); await newTabOpened; assertFirefoxViewTab(win); ok( diff --git a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js index c4c096acff..847ce4d9fd 100644 --- a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js @@ -58,14 +58,14 @@ function isElInViewport(element) { async function historyComponentReady(historyComponent, expectedHistoryItems) { await TestUtils.waitForCondition( () => - [...historyComponent.allHistoryItems.values()].reduce( + [...historyComponent.controller.allHistoryItems.values()].reduce( (acc, { length }) => acc + length, 0 ) === expectedHistoryItems, "History component ready" ); - let expected = historyComponent.historyMapByDate.length; + let expected = historyComponent.controller.historyMapByDate.length; let actual = historyComponent.cards.length; is(expected, actual, `Total number of cards should be ${expected}`); @@ -242,7 +242,8 @@ add_task(async function test_list_ordering() { await TestUtils.waitForCondition(() => historyComponent.fullyUpdated); await sortHistoryTelemetry(sortHistoryEvent); - let expectedNumOfCards = historyComponent.historyMapBySite.length; + let expectedNumOfCards = + historyComponent.controller.historyMapBySite.length; info(`Total number of cards should be ${expectedNumOfCards}`); await BrowserTestUtils.waitForMutationCondition( @@ -345,7 +346,7 @@ add_task(async function test_empty_states() { "Import history banner is shown" ); let importHistoryCloseButton = - historyComponent.cards[0].querySelector("button.close"); + historyComponent.cards[0].querySelector("moz-button.close"); importHistoryCloseButton.click(); await TestUtils.waitForCondition(() => historyComponent.fullyUpdated); ok( @@ -484,7 +485,7 @@ add_task(async function test_search_history() { { childList: true, subtree: true }, () => historyComponent.cards.length === - historyComponent.historyMapByDate.length + historyComponent.controller.historyMapByDate.length ); searchTextbox.blur(); @@ -513,7 +514,7 @@ add_task(async function test_search_history() { { childList: true, subtree: true }, () => historyComponent.cards.length === - historyComponent.historyMapByDate.length + historyComponent.controller.historyMapByDate.length ); }); }); @@ -528,7 +529,7 @@ add_task(async function test_persist_collapse_card_after_view_change() { historyComponent.profileAge = 8; await TestUtils.waitForCondition( () => - [...historyComponent.allHistoryItems.values()].reduce( + [...historyComponent.controller.allHistoryItems.values()].reduce( (acc, { length }) => acc + length, 0 ) === 4 diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js b/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js index d4de3ae5a9..5fdcf89d70 100644 --- a/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js @@ -203,13 +203,15 @@ add_task(async function open_tab_new_window() { const cards = getOpenTabsCards(openTabs); const originalWinRows = await getTabRowsForCard(cards[1]); const [row] = originalWinRows; + + // We hide date/time and URL columns in tab rows when there are multiple window cards for spacial reasons ok( - row.shadowRoot.getElementById("fxview-tab-row-url").hidden, - "The URL is hidden, since we have two windows." + !row.shadowRoot.getElementById("fxview-tab-row-url"), + "The URL span element isn't found within the tab row as expected, since we have two open windows." ); ok( - row.shadowRoot.getElementById("fxview-tab-row-date").hidden, - "The date is hidden, since we have two windows." + !row.shadowRoot.getElementById("fxview-tab-row-date"), + "The date span element isn't found within the tab row as expected, since we have two open windows." ); info("Select a tab from the original window."); tabChangeRaised = BrowserTestUtils.waitForEvent( diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js index 955c2363d7..2c415e7aa2 100644 --- a/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js @@ -131,7 +131,7 @@ async function moreMenuSetup() { } add_task(async function test_close_open_tab() { - await withFirefoxView({}, async browser => { + await withFirefoxView({}, async () => { const [cards, rows] = await moreMenuSetup(); const firstTab = rows[0]; const tertiaryButtonEl = firstTab.tertiaryButtonEl; @@ -321,7 +321,7 @@ add_task(async function test_send_device_submenu() { .stub(gSync, "getSendTabTargets") .callsFake(() => fxaDevicesWithCommands); - await withFirefoxView({}, async browser => { + await withFirefoxView({}, async () => { // TEST_URL1 is our only tab, left over from previous test Assert.deepEqual( getVisibleTabURLs(), diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js b/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js index ee3f9981e1..fc10ef2eb0 100644 --- a/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js @@ -2,23 +2,30 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ /* - This test checks the recent-browsing view of open tabs in about:firefoxview next + This test checks that the recent-browsing view of open tabs in about:firefoxview presents the correct tab data in the correct order. */ +SimpleTest.requestCompleteLog(); + +const { ObjectUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ObjectUtils.sys.mjs" +); +let origBrowserState; const tabURL1 = "data:,Tab1"; const tabURL2 = "data:,Tab2"; const tabURL3 = "data:,Tab3"; const tabURL4 = "data:,Tab4"; -let gInitialTab; -let gInitialTabURL; - add_setup(function () { - gInitialTab = gBrowser.selectedTab; - gInitialTabURL = tabUrl(gInitialTab); + origBrowserState = SessionStore.getBrowserState(); }); +async function cleanup() { + await switchToWindow(window); + await SessionStoreTestUtils.promiseBrowserState(origBrowserState); +} + function tabUrl(tab) { return tab.linkedBrowser.currentURI?.spec; } @@ -37,6 +44,12 @@ async function minimizeWindow(win) { ok(win.document.hidden, "Top level window should be hidden"); } +function getAllSelectedTabURLs() { + return BrowserWindowTracker.orderedWindows.map(win => + tabUrl(win.gBrowser.selectedTab) + ); +} + async function restoreWindow(win) { ok(win.document.hidden, "Top level window should be hidden"); let promiseSizeModeChange = BrowserTestUtils.waitForEvent( @@ -93,86 +106,91 @@ async function restoreWindow(win) { ok(!win.document.hidden, "Top level window should be visible"); } -async function prepareOpenTabs(urls, win = window) { - const reusableTabURLs = ["about:newtab", "about:blank"]; - const gBrowser = win.gBrowser; - - for (let url of urls) { - if ( - gBrowser.visibleTabs.length == 1 && - reusableTabURLs.includes(gBrowser.selectedBrowser.currentURI.spec) - ) { - // we'll load into this tab rather than opening a new one - info( - `Loading ${url} into blank tab: ${gBrowser.selectedBrowser.currentURI.spec}` - ); - BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url); - await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, null, url); - } else { - info(`Loading ${url} into new tab`); - await BrowserTestUtils.openNewForegroundTab(gBrowser, url); - } - await new Promise(res => win.requestAnimationFrame(res)); +async function prepareOpenWindowsAndTabs(windowsData) { + // windowsData selected tab URL should be unique so we can map tab URL to window + const browserState = { + windows: windowsData.map((winData, index) => { + const tabs = winData.tabs.map(url => ({ + entries: [{ url, triggeringPrincipal_base64 }], + })); + return { + tabs, + selected: winData.selectedIndex + 1, + zIndex: index + 1, + }; + }), + }; + await SessionStoreTestUtils.promiseBrowserState(browserState); + await NonPrivateTabs.readyWindowsPromise; + const selectedTabURLOrder = browserState.windows.map(winData => { + return winData.tabs[winData.selected - 1].entries[0].url; + }); + const windowByTabURL = new Map(); + for (let win of BrowserWindowTracker.orderedWindows) { + windowByTabURL.set(tabUrl(win.gBrowser.selectedTab), win); } - Assert.equal( - gBrowser.visibleTabs.length, - urls.length, - `Prepared ${urls.length} tabs as expected` - ); - Assert.equal( - tabUrl(gBrowser.selectedTab), - urls[urls.length - 1], - "The selectedTab is the last of the URLs given as expected" + is( + windowByTabURL.size, + windowsData.length, + "The tab URL to window mapping includes an entry for each window" ); -} - -async function cleanup(...windowsToClose) { - await Promise.all( - windowsToClose.map(win => BrowserTestUtils.closeWindow(win)) + info( + `After promiseBrowserState, selected tab order is: ${Array.from( + windowByTabURL.keys() + )}` ); - while (gBrowser.visibleTabs.length > 1) { - await SessionStoreTestUtils.closeTab(gBrowser.tabs.at(-1)); - } - if (gBrowser.selectedBrowser.currentURI.spec !== gInitialTabURL) { - BrowserTestUtils.startLoadingURIString( - gBrowser.selectedBrowser, - gInitialTabURL - ); - await BrowserTestUtils.browserLoaded( - gBrowser.selectedBrowser, - null, - gInitialTabURL - ); + // Make any corrections to the window order by selecting each in reverse order + for (let url of selectedTabURLOrder.toReversed()) { + await switchToWindow(windowByTabURL.get(url)); } + // Verify windows are in the expected order + Assert.deepEqual( + getAllSelectedTabURLs(), + selectedTabURLOrder, + "The windows and their selected tabs are in the expected order" + ); + Assert.deepEqual( + BrowserWindowTracker.orderedWindows.map(win => + win.gBrowser.visibleTabs.map(tab => tabUrl(tab)) + ), + windowsData.map(winData => winData.tabs), + "We opened all the tabs in each window" + ); } -function getOpenTabsComponent(browser) { +function getRecentOpenTabsComponent(browser) { return browser.contentDocument.querySelector( "view-recentbrowsing view-opentabs" ); } -async function checkTabList(browser, expected) { - const tabsView = getOpenTabsComponent(browser); +async function checkRecentTabList(browser, expected) { + const tabsView = getRecentOpenTabsComponent(browser); const [openTabsCard] = getOpenTabsCards(tabsView); await openTabsCard.updateComplete; const tabListRows = await getTabRowsForCard(openTabsCard); Assert.ok(tabListRows, "Found the tab list element"); let actual = Array.from(tabListRows).map(row => row.url); - Assert.deepEqual( - actual, - expected, - "Tab list has items with URLs in the expected order" + await BrowserTestUtils.waitForCondition( + () => ObjectUtils.deepEqual(actual, expected), + "Waiting for tab list to hvae items with URLs in the expected order" ); } add_task(async function test_single_window_tabs() { - await prepareOpenTabs([tabURL1, tabURL2]); + const testData = [ + { + tabs: [tabURL1, tabURL2], + selectedIndex: 1, // the 2nd tab should be selected + }, + ]; + await prepareOpenWindowsAndTabs(testData); + await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; - await checkTabList(browser, [tabURL2, tabURL1]); + await checkRecentTabList(browser, [tabURL2, tabURL1]); // switch to the first tab let promiseHidden = BrowserTestUtils.waitForEvent( @@ -192,25 +210,62 @@ add_task(async function test_single_window_tabs() { // and check the results in the open tabs section of Recent Browsing await openFirefoxViewTab(window).then(async viewTab => { const browser = viewTab.linkedBrowser; - await checkTabList(browser, [tabURL1, tabURL2]); + await checkRecentTabList(browser, [tabURL1, tabURL2]); }); await cleanup(); }); add_task(async function test_multiple_window_tabs() { const fxViewURL = getFirefoxViewURL(); - const win1 = window; + const testData = [ + { + // this window should be active after restore + tabs: [tabURL1, tabURL2], + selectedIndex: 0, // tabURL1 should be selected + }, + { + tabs: [tabURL3, tabURL4], + selectedIndex: 0, // tabURL3 should be selected + }, + ]; + await prepareOpenWindowsAndTabs(testData); + + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL1, tabURL3], + "The windows and their selected tabs are in the expected order" + ); let tabChangeRaised; - await prepareOpenTabs([tabURL1, tabURL2]); - const win2 = await BrowserTestUtils.openNewBrowserWindow(); - await prepareOpenTabs([tabURL3, tabURL4], win2); + const [win1, win2] = BrowserWindowTracker.orderedWindows; + + info(`Switch to window 1's 2nd tab: ${tabUrl(win1.gBrowser.visibleTabs[1])}`); + await BrowserTestUtils.switchTab(gBrowser, win1.gBrowser.visibleTabs[1]); + await switchToWindow(win2); + + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL3, tabURL2], + `Window 2 has selected the ${tabURL3} tab, window 1 has ${tabURL2}` + ); + info(`Switch to window 2's 2nd tab: ${tabUrl(win2.gBrowser.visibleTabs[1])}`); + tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabRecencyChange" + ); + await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[1]); + await tabChangeRaised; + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL4, tabURL2], + `window 2 has selected the ${tabURL4} tab, ${tabURL2} remains selected in window 1` + ); // to avoid confusing the results by activating different windows, // check fxview in the current window - which is win2 info("Switching to fxview tab in win2"); await openFirefoxViewTab(win2).then(async viewTab => { const browser = viewTab.linkedBrowser; - await checkTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]); + await checkRecentTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]); Assert.equal( tabUrl(win2.gBrowser.selectedTab), @@ -218,7 +273,7 @@ add_task(async function test_multiple_window_tabs() { `The selected tab in window 2 is ${fxViewURL}` ); - info("Switching to first tab (tab3) in win2"); + info("Switching to first tab in win2"); tabChangeRaised = BrowserTestUtils.waitForEvent( NonPrivateTabs, "TabRecencyChange" @@ -231,20 +286,20 @@ add_task(async function test_multiple_window_tabs() { win2.gBrowser, win2.gBrowser.visibleTabs[0] ); - Assert.equal( - tabUrl(win2.gBrowser.selectedTab), - tabURL3, - `The selected tab in window 2 is ${tabURL3}` - ); await tabChangeRaised; await promiseHidden; + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL3, tabURL2], + `window 2 has switched to ${tabURL3}, ${tabURL2} remains selected in window 1` + ); }); info("Opening fxview in win2 to confirm tab3 is most recent"); await openFirefoxViewTab(win2).then(async viewTab => { const browser = viewTab.linkedBrowser; info("Check result of selecting 1ist tab in window 2"); - await checkTabList(browser, [tabURL3, tabURL4, tabURL2, tabURL1]); + await checkRecentTabList(browser, [tabURL3, tabURL4, tabURL2, tabURL1]); }); info("Focusing win1, where tab2 should be selected"); @@ -254,10 +309,10 @@ add_task(async function test_multiple_window_tabs() { ); await switchToWindow(win1); await tabChangeRaised; - Assert.equal( - tabUrl(win1.gBrowser.selectedTab), - tabURL2, - `The selected tab in window 1 is ${tabURL2}` + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL2, fxViewURL], + `The selected tab in window 1 is ${tabURL2}, ${fxViewURL} remains selected in window 2` ); info("Opening fxview in win1 to confirm tab2 is most recent"); @@ -266,7 +321,7 @@ add_task(async function test_multiple_window_tabs() { info( "In fxview, check result of activating window 1, where tab 2 is selected" ); - await checkTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]); + await checkRecentTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]); let promiseHidden = BrowserTestUtils.waitForEvent( browser.contentDocument, @@ -284,45 +339,50 @@ add_task(async function test_multiple_window_tabs() { await promiseHidden; await tabChangeRaised; }); + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL1, fxViewURL], + `The selected tab in window 1 is ${tabURL1}, ${fxViewURL} remains selected in window 2` + ); // check result in the fxview in the 1st window info("Opening fxview in win1 to confirm tab1 is most recent"); await openFirefoxViewTab(win1).then(async viewTab => { const browser = viewTab.linkedBrowser; info("Check result of selecting 1st tab in win1"); - await checkTabList(browser, [tabURL1, tabURL2, tabURL3, tabURL4]); + await checkRecentTabList(browser, [tabURL1, tabURL2, tabURL3, tabURL4]); }); - await cleanup(win2); + await cleanup(); }); add_task(async function test_windows_activation() { - const win1 = window; - await prepareOpenTabs([tabURL1], win1); - let fxViewTab; - let tabChangeRaised; - info("switch to firefox-view and leave it selected"); - await openFirefoxViewTab(win1).then(tab => (fxViewTab = tab)); + // use Session restore to batch-open windows and tabs + const testData = [ + { + // this window should be active after restore + tabs: [tabURL1], + selectedIndex: 0, // tabURL1 should be selected + }, + { + tabs: [tabURL2], + selectedIndex: 0, // tabURL2 should be selected + }, + { + tabs: [tabURL3], + selectedIndex: 0, // tabURL3 should be selected + }, + ]; + await prepareOpenWindowsAndTabs(testData); - const win2 = await BrowserTestUtils.openNewBrowserWindow(); - await switchToWindow(win2); - await prepareOpenTabs([tabURL2], win2); - - const win3 = await BrowserTestUtils.openNewBrowserWindow(); - await switchToWindow(win3); - await prepareOpenTabs([tabURL3], win3); - - tabChangeRaised = BrowserTestUtils.waitForEvent( - NonPrivateTabs, - "TabRecencyChange" - ); - info("Switching back to win 1"); - await switchToWindow(win1); - info("Waiting for tabChangeRaised to resolve"); - await tabChangeRaised; + let tabChangeRaised; + const [win1, win2] = BrowserWindowTracker.orderedWindows; - const browser = fxViewTab.linkedBrowser; - await checkTabList(browser, [tabURL3, tabURL2, tabURL1]); + info("switch to firefox-view and leave it selected"); + await openFirefoxViewTab(win1).then(async viewTab => { + const browser = viewTab.linkedBrowser; + await checkRecentTabList(browser, [tabURL1, tabURL2, tabURL3]); + }); info("switch to win2 and confirm its selected tab becomes most recent"); tabChangeRaised = BrowserTestUtils.waitForEvent( @@ -331,24 +391,52 @@ add_task(async function test_windows_activation() { ); await switchToWindow(win2); await tabChangeRaised; - await checkTabList(browser, [tabURL2, tabURL3, tabURL1]); - await cleanup(win2, win3); + await openFirefoxViewTab(win1).then(async viewTab => { + await checkRecentTabList(viewTab.linkedBrowser, [ + tabURL2, + tabURL1, + tabURL3, + ]); + }); + await cleanup(); }); add_task(async function test_minimize_restore_windows() { - const win1 = window; - let tabChangeRaised; - await prepareOpenTabs([tabURL1, tabURL2]); - const win2 = await BrowserTestUtils.openNewBrowserWindow(); - await prepareOpenTabs([tabURL3, tabURL4], win2); - await NonPrivateTabs.readyWindowsPromise; + const fxViewURL = getFirefoxViewURL(); + const testData = [ + { + // this window should be active after restore + tabs: [tabURL1, tabURL2], + selectedIndex: 1, // tabURL2 should be selected + }, + { + tabs: [tabURL3, tabURL4], + selectedIndex: 0, // tabURL3 should be selected + }, + ]; + await prepareOpenWindowsAndTabs(testData); + const [win1, win2] = BrowserWindowTracker.orderedWindows; + + // switch to the last (tabURL4) tab in window 2 + await switchToWindow(win2); + let tabChangeRaised = BrowserTestUtils.waitForEvent( + NonPrivateTabs, + "TabRecencyChange" + ); + await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[1]); + await tabChangeRaised; + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL4, tabURL2], + "The windows and their selected tabs are in the expected order" + ); // to avoid confusing the results by activating different windows, // check fxview in the current window - which is win2 info("Opening fxview in win2 to confirm tab4 is most recent"); await openFirefoxViewTab(win2).then(async viewTab => { const browser = viewTab.linkedBrowser; - await checkTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]); + await checkRecentTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]); let promiseHidden = BrowserTestUtils.waitForEvent( browser.contentDocument, @@ -366,6 +454,11 @@ add_task(async function test_minimize_restore_windows() { await promiseHidden; await tabChangeRaised; }); + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL3, tabURL2], + `Window 2 has ${tabURL3} selected, window 1 remains at ${tabURL2}` + ); // then minimize the window, focusing the 1st window info("Minimizing win2, leaving tab 3 selected"); @@ -378,32 +471,41 @@ add_task(async function test_minimize_restore_windows() { await switchToWindow(win1); await tabChangeRaised; - Assert.equal( - tabUrl(win1.gBrowser.selectedTab), - tabURL2, - `The selected tab in window 1 is ${tabURL2}` + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL2, tabURL3], + `Window 1 has ${tabURL2} selected, window 2 remains at ${tabURL3}` ); info("Opening fxview in win1 to confirm tab2 is most recent"); await openFirefoxViewTab(win1).then(async viewTab => { const browser = viewTab.linkedBrowser; - await checkTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]); + await checkRecentTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]); info( "Restoring win2 and focusing it - which should make its selected tab most recent" ); tabChangeRaised = BrowserTestUtils.waitForEvent( NonPrivateTabs, - "TabRecencyChange" + "TabRecencyChange", + false, + event => event.detail.sourceEvents?.includes("activate") ); await restoreWindow(win2); await switchToWindow(win2); + // make sure we wait for the activate event from OpenTabs. await tabChangeRaised; + Assert.deepEqual( + getAllSelectedTabURLs(), + [tabURL3, fxViewURL], + `Window 2 was restored and has ${tabURL3} selected, window 1 remains at ${fxViewURL}` + ); + info( "Checking tab order in fxview in win1, to confirm tab3 is most recent" ); - await checkTabList(browser, [tabURL3, tabURL2, tabURL4, tabURL1]); + await checkRecentTabList(browser, [tabURL3, tabURL2, tabURL4, tabURL1]); }); - - await cleanup(win2); + info("test done, waiting for cleanup"); + await cleanup(); }); diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js b/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js index 78fab976ed..4403a8e36a 100644 --- a/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js +++ b/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js @@ -94,12 +94,16 @@ add_task(async function test_container_indicator() { await TestUtils.waitForCondition( () => Array.from(openTabs.viewCards[0].tabList.rowEls).some(rowEl => { - containerTabElem = rowEl; - return rowEl.containerObj; + let hasContainerObj; + if (rowEl.containerObj?.icon) { + containerTabElem = rowEl; + hasContainerObj = rowEl.containerObj; + } + + return hasContainerObj; }), "The container tab element isn't marked in Fx View." ); - ok( containerTabElem.shadowRoot .querySelector(".fxview-tab-row-container-indicator") diff --git a/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js index fcfcf20562..85879667bb 100644 --- a/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js @@ -372,6 +372,12 @@ add_task(async function test_dismiss_tab() { info("calling dismiss_tab on the top, most-recently closed tab"); let closedTabItem = listItems[0]; + // the most recently closed tab was in window 3 which got closed + // so we expect a sourceClosedId on the item element + ok( + !isNaN(closedTabItem.sourceClosedId), + "Item has a sourceClosedId property" + ); // dismiss the first tab and verify the list is correctly updated await dismiss_tab(closedTabItem); @@ -390,6 +396,12 @@ add_task(async function test_dismiss_tab() { // dismiss the last tab and verify the list is correctly updated closedTabItem = listItems[listItems.length - 1]; + ok( + isNaN(closedTabItem.sourceClosedId), + "Item does not have a sourceClosedId property" + ); + ok(closedTabItem.sourceWindowId, "Item has a sourceWindowId property"); + await dismiss_tab(closedTabItem); await listElem.getUpdateComplete; diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js index 86e4d9cdee..a644b39fc6 100644 --- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js @@ -69,19 +69,23 @@ add_task(async function test_network_offline() { "view-syncedtabs:not([slot=syncedtabs])" ); await TestUtils.waitForCondition(() => syncedTabsComponent.fullyUpdated); - await BrowserTestUtils.waitForMutationCondition( - syncedTabsComponent.shadowRoot.querySelector(".cards-container"), - { childList: true }, - () => syncedTabsComponent.shadowRoot.innerHTML.includes("network-offline") + await TestUtils.waitForCondition( + () => + syncedTabsComponent.emptyState.shadowRoot.textContent.includes( + "Check your internet connection" + ), + "The expected network offline error message is displayed." ); - let emptyState = - syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state"); ok( - emptyState.getAttribute("headerlabel").includes("network-offline"), + syncedTabsComponent.emptyState + .getAttribute("headerlabel") + .includes("network-offline"), "Network offline message is shown" ); - emptyState.querySelector("button[data-action='network-offline']").click(); + syncedTabsComponent.emptyState + .querySelector("button[data-action='network-offline']") + .click(); await BrowserTestUtils.waitForCondition( () => TabsSetupFlowManager.tryToClearError.calledOnce @@ -92,10 +96,10 @@ add_task(async function test_network_offline() { "TabsSetupFlowManager.tryToClearError() was called once" ); - emptyState = - syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state"); ok( - emptyState.getAttribute("headerlabel").includes("network-offline"), + syncedTabsComponent.emptyState + .getAttribute("headerlabel") + .includes("network-offline"), "Network offline message is still shown" ); @@ -121,16 +125,18 @@ add_task(async function test_sync_error() { "view-syncedtabs:not([slot=syncedtabs])" ); await TestUtils.waitForCondition(() => syncedTabsComponent.fullyUpdated); - await BrowserTestUtils.waitForMutationCondition( - syncedTabsComponent.shadowRoot.querySelector(".cards-container"), - { childList: true }, - () => syncedTabsComponent.shadowRoot.innerHTML.includes("sync-error") + await TestUtils.waitForCondition( + () => + syncedTabsComponent.emptyState.shadowRoot.textContent.includes( + "having trouble syncing" + ), + "Sync error message is shown." ); - let emptyState = - syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state"); ok( - emptyState.getAttribute("headerlabel").includes("sync-error"), + syncedTabsComponent.emptyState + .getAttribute("headerlabel") + .includes("sync-error"), "Correct message should show when there's a sync service error" ); @@ -139,3 +145,233 @@ add_task(async function test_sync_error() { }); await tearDown(sandbox); }); + +add_task(async function test_sync_disabled_by_policy() { + await SpecialPowers.pushPrefEnv({ + set: [["identity.fxaccounts.enabled", false]], + }); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + const recentBrowsingSyncedTabs = document.querySelector( + "view-syncedtabs[slot=syncedtabs]" + ); + const syncedtabsPageNavButton = document.querySelector( + "moz-page-nav-button[view='syncedtabs']" + ); + + ok( + BrowserTestUtils.isHidden(recentBrowsingSyncedTabs), + "Synced tabs should not be visible from recent browsing." + ); + ok( + BrowserTestUtils.isHidden(syncedtabsPageNavButton), + "Synced tabs nav button should not be visible." + ); + + document.location.assign(`${getFirefoxViewURL()}#syncedtabs`); + await TestUtils.waitForTick(); + is( + document.querySelector("moz-page-nav").currentView, + "recentbrowsing", + "Should not be able to navigate to synced tabs." + ); + }); + await tearDown(); +}); + +add_task(async function test_sync_error_signed_out() { + // sync error should not show if user is not signed in + let sandbox = await setupWithDesktopDevices(UIState.STATUS_NOT_CONFIGURED); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + await navigateToViewAndWait(document, "syncedtabs"); + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + Services.obs.notifyObservers(null, "weave:service:sync:error"); + + let syncedTabsComponent = document.querySelector( + "view-syncedtabs:not([slot=syncedtabs])" + ); + await TestUtils.waitForCondition( + () => syncedTabsComponent.fullyUpdated, + "The synced tabs component has finished updating." + ); + await TestUtils.waitForCondition( + () => + syncedTabsComponent.emptyState.shadowRoot.textContent.includes( + "sign in to your account" + ), + "Sign in header is shown." + ); + + ok( + syncedTabsComponent.emptyState + .getAttribute("headerlabel") + .includes("signin-header"), + "Sign in message is shown" + ); + }); + await tearDown(sandbox); +}); + +add_task(async function test_sync_disconnected_error() { + // it's possible for fxa to be enabled but sync not enabled. + const sandbox = setupSyncFxAMocks({ + state: UIState.STATUS_SIGNED_IN, + syncEnabled: false, + }); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + await navigateToViewAndWait(document, "syncedtabs"); + + // triggered when user disconnects sync in about:preferences + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + + let syncedTabsComponent = document.querySelector( + "view-syncedtabs:not([slot=syncedtabs])" + ); + info("Waiting for the synced tabs error step to be visible"); + await TestUtils.waitForCondition( + () => syncedTabsComponent.fullyUpdated, + "The synced tabs component has finished updating." + ); + await TestUtils.waitForCondition( + () => + syncedTabsComponent.emptyState.shadowRoot.textContent.includes( + "allow syncing" + ), + "The expected synced tabs empty state header is shown." + ); + + info( + "Waiting for a mutation condition to ensure the right syncing error message" + ); + ok( + syncedTabsComponent.emptyState + .getAttribute("headerlabel") + .includes("sync-disconnected-header"), + "Correct message should show when sync's been disconnected error" + ); + + let preferencesTabPromise = BrowserTestUtils.waitForNewTab( + browser.getTabBrowser(), + "about:preferences?action=choose-what-to-sync#sync", + true + ); + let emptyStateButton = syncedTabsComponent.emptyState.querySelector( + "button[data-action='sync-disconnected']" + ); + EventUtils.synthesizeMouseAtCenter(emptyStateButton, {}, content); + let preferencesTab = await preferencesTabPromise; + await BrowserTestUtils.removeTab(preferencesTab); + }); + await tearDown(sandbox); +}); + +add_task(async function test_password_change_disconnect_error() { + // When the user changes their password on another device, we get into a state + // where the user is signed out but sync is still enabled. + const sandbox = setupSyncFxAMocks({ + state: UIState.STATUS_LOGIN_FAILED, + syncEnabled: true, + }); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + await navigateToViewAndWait(document, "syncedtabs"); + + // triggered by the user changing fxa password on another device + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + + let syncedTabsComponent = document.querySelector( + "view-syncedtabs:not([slot=syncedtabs])" + ); + await TestUtils.waitForCondition( + () => syncedTabsComponent.fullyUpdated, + "The synced tabs component has finished updating." + ); + await TestUtils.waitForCondition( + () => + syncedTabsComponent.emptyState.shadowRoot.textContent.includes( + "sign in to your account" + ), + "The expected synced tabs empty state header is shown." + ); + + ok( + syncedTabsComponent.emptyState + .getAttribute("headerlabel") + .includes("signin-header"), + "Sign in message is shown" + ); + }); + await tearDown(sandbox); +}); + +add_task(async function test_multiple_errors() { + let sandbox = await setupWithDesktopDevices(); + await withFirefoxView({}, async browser => { + const { document } = browser.contentWindow; + await navigateToViewAndWait(document, "syncedtabs"); + // Simulate conditions in which both the locked password and sync error + // messages could be shown + LoginTestUtils.primaryPassword.enable(); + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + Services.obs.notifyObservers(null, "weave:service:sync:error"); + + let syncedTabsComponent = document.querySelector( + "view-syncedtabs:not([slot=syncedtabs])" + ); + await TestUtils.waitForCondition( + () => syncedTabsComponent.fullyUpdated, + "The synced tabs component has finished updating." + ); + info("Waiting for the primary password error message to be shown"); + await TestUtils.waitForCondition( + () => + syncedTabsComponent.emptyState.shadowRoot.textContent.includes( + "enter the Primary Password" + ), + "The expected synced tabs empty state header is shown." + ); + + ok( + syncedTabsComponent.emptyState + .getAttribute("headerlabel") + .includes("password-locked-header"), + "Password locked message is shown" + ); + + const errorLink = syncedTabsComponent.emptyState.shadowRoot.querySelector( + "a[data-l10n-name=syncedtab-password-locked-link]" + ); + ok( + errorLink && BrowserTestUtils.isVisible(errorLink), + "Error link is visible" + ); + + // Clear the primary password error message + LoginTestUtils.primaryPassword.disable(); + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + + info("Waiting for the sync error message to be shown"); + await TestUtils.waitForCondition( + () => syncedTabsComponent.fullyUpdated, + "The synced tabs component has finished updating." + ); + await TestUtils.waitForCondition( + () => + syncedTabsComponent.emptyState.shadowRoot.textContent.includes( + "having trouble syncing" + ), + "The expected synced tabs empty state header is shown." + ); + + ok( + errorLink && BrowserTestUtils.isHidden(errorLink), + "Error link is now hidden" + ); + + // Clear the sync error + Services.obs.notifyObservers(null, "weave:service:sync:finish"); + }); + await tearDown(sandbox); +}); diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js index 11f135cd52..1bf387f578 100644 --- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js @@ -276,9 +276,12 @@ add_task(async function test_tabs() { }); await withFirefoxView({ openNewWindow: true }, async browser => { + // Notify observers while in recent browsing. Once synced tabs is selected, + // it should have the updated data. + Services.obs.notifyObservers(null, UIState.ON_UPDATE); + const { document } = browser.contentWindow; await navigateToViewAndWait(document, "syncedtabs"); - Services.obs.notifyObservers(null, UIState.ON_UPDATE); let syncedTabsComponent = document.querySelector( "view-syncedtabs:not([slot=syncedtabs])" @@ -309,7 +312,7 @@ add_task(async function test_tabs() { ); ok(tabRow1[1].shadowRoot.textContent.includes, "Sandboxes - Sinon.JS"); is(tabRow1.length, 2, "Correct number of rows are displayed."); - let tabRow2 = tabLists[1].shadowRoot.querySelectorAll("fxview-tab-row"); + let tabRow2 = tabLists[1].rowEls; is(tabRow2.length, 2, "Correct number of rows are dispayed."); ok(tabRow1[0].shadowRoot.textContent.includes, "The Guardian"); ok(tabRow1[1].shadowRoot.textContent.includes, "The Times"); diff --git a/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js b/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js index d83c1056e0..270c3b6809 100644 --- a/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js +++ b/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js @@ -93,7 +93,7 @@ add_task(async function test_focus_moves_after_unmute() { ); // Unmute using keyboard - card.tabList.currentActiveElementId = mutedTab.focusMediaButton(); + mutedTab.focusMediaButton(); isActiveElement(mutedTab.mediaButtonEl); info("The media button has focus."); @@ -124,7 +124,7 @@ add_task(async function test_focus_moves_after_unmute() { ); mutedTab = card.tabList.rowEls[0]; - card.tabList.currentActiveElementId = mutedTab.focusLink(); + mutedTab.focusLink(); isActiveElement(mutedTab.mainEl); info("The 'main' element has focus."); diff --git a/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js b/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js index 9980980c29..a63a55163a 100644 --- a/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js +++ b/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js @@ -31,7 +31,7 @@ add_task( info("Opening Firefox View tab..."); await openFirefoxViewTab(win); info("Trigger warnAboutClosingWindow()"); - win.BrowserTryToCloseWindow(); + win.BrowserCommands.tryToCloseWindow(); await BrowserTestUtils.closeWindow(win); ok(!dialogObserver.wasOpened, "Dialog was not opened"); dialogObserver.cleanup(); diff --git a/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html b/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html index e48f776592..52ddc277c7 100644 --- a/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html +++ b/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html @@ -11,11 +11,6 @@ <script type="module" src="chrome://browser/content/firefoxview/fxview-tab-list.mjs"></script> </head> <body> - <style> - fxview-tab-list.history::part(secondary-button) { - background-image: url("chrome://global/skin/icons/more.svg"); - } - </style> <p id="display"></p> <div id="content" style="max-width: 750px"> <fxview-tab-list class="history" .dateTimeFormat="relative" .hasPopup="menu"> diff --git a/browser/components/ion/content/ion.js b/browser/components/ion/content/ion.js index 3c34328d58..ef3217d239 100644 --- a/browser/components/ion/content/ion.js +++ b/browser/components/ion/content/ion.js @@ -444,7 +444,7 @@ async function setup(cachedAddons) { document .getElementById("join-ion-accept-dialog-button") - .addEventListener("click", async event => { + .addEventListener("click", async () => { const ionId = Services.prefs.getStringPref(PREF_ION_ID, null); if (!ionId) { @@ -501,7 +501,7 @@ async function setup(cachedAddons) { document .getElementById("leave-ion-accept-dialog-button") - .addEventListener("click", async event => { + .addEventListener("click", async () => { const completedStudies = Services.prefs.getStringPref( PREF_ION_COMPLETED_STUDIES, "{}" @@ -567,7 +567,7 @@ async function setup(cachedAddons) { document .getElementById("join-study-accept-dialog-button") - .addEventListener("click", async event => { + .addEventListener("click", async () => { const dialog = document.getElementById("join-study-consent-dialog"); const studyAddonId = dialog.getAttribute("addon-id"); toggleEnrolled(studyAddonId, cachedAddons).then(dialog.close()); @@ -575,7 +575,7 @@ async function setup(cachedAddons) { document .getElementById("leave-study-accept-dialog-button") - .addEventListener("click", async event => { + .addEventListener("click", async () => { const dialog = document.getElementById("leave-study-consent-dialog"); const studyAddonId = dialog.getAttribute("addon-id"); await toggleEnrolled(studyAddonId, cachedAddons).then(dialog.close()); @@ -597,7 +597,7 @@ async function setup(cachedAddons) { }; AddonManager.addAddonListener(addonsListener); - window.addEventListener("unload", event => { + window.addEventListener("unload", () => { AddonManager.removeAddonListener(addonsListener); }); } @@ -639,7 +639,7 @@ function updateContents(contents) { } } -document.addEventListener("DOMContentLoaded", async domEvent => { +document.addEventListener("DOMContentLoaded", async () => { toggleContentBasedOnLocale(); showEnrollmentStatus(); diff --git a/browser/components/ion/test/browser/browser_ion_ui.js b/browser/components/ion/test/browser/browser_ion_ui.js index e956cefa25..3cf3c47f96 100644 --- a/browser/components/ion/test/browser/browser_ion_ui.js +++ b/browser/components/ion/test/browser/browser_ion_ui.js @@ -321,7 +321,7 @@ add_task(async function testBadDefaultAddon() { url: "about:ion", gBrowser, }, - async function taskFn(browser) { + async function taskFn() { const beforePref = Services.prefs.getStringPref(PREF_ION_ID, null); Assert.strictEqual( beforePref, @@ -402,7 +402,7 @@ add_task(async function testAboutPage() { url: "about:ion", gBrowser, }, - async function taskFn(browser) { + async function taskFn() { const beforePref = Services.prefs.getStringPref(PREF_ION_ID, null); Assert.strictEqual( beforePref, @@ -694,19 +694,16 @@ add_task(async function testAboutPage() { // Wait for deletion ping, uninstalls, and UI updates... const ionUnenrolled = await new Promise((resolve, reject) => { - Services.prefs.addObserver( - PREF_ION_ID, - function observer(subject, topic, data) { - try { - const prefValue = Services.prefs.getStringPref(PREF_ION_ID, null); - Services.prefs.removeObserver(PREF_ION_ID, observer); - resolve(prefValue); - } catch (ex) { - Services.prefs.removeObserver(PREF_ION_ID, observer); - reject(ex); - } + Services.prefs.addObserver(PREF_ION_ID, function observer() { + try { + const prefValue = Services.prefs.getStringPref(PREF_ION_ID, null); + Services.prefs.removeObserver(PREF_ION_ID, observer); + resolve(prefValue); + } catch (ex) { + Services.prefs.removeObserver(PREF_ION_ID, observer); + reject(ex); } - ); + }); }); ok(!ionUnenrolled, "after accepting unenrollment, Ion pref is null."); @@ -795,7 +792,7 @@ add_task(async function testEnrollmentPings() { url: "about:ion", gBrowser, }, - async function taskFn(browser) { + async function taskFn() { const beforePref = Services.prefs.getStringPref(PREF_ION_ID, null); Assert.strictEqual( beforePref, @@ -984,7 +981,7 @@ add_task(async function testContentReplacement() { url: "about:ion", gBrowser, }, - async function taskFn(browser) { + async function taskFn() { // Check that text was updated from Remote Settings. console.log("debug:", content.document.getElementById("title").innerHTML); Assert.equal( @@ -1042,7 +1039,7 @@ add_task(async function testBadContentReplacement() { url: "about:ion", gBrowser, }, - async function taskFn(browser) { + async function taskFn() { // Check that text was updated from Remote Settings. Assert.equal( content.document.getElementById("join-ion-consent").innerHTML, @@ -1081,7 +1078,7 @@ add_task(async function testLocaleGating() { url: "about:ion", gBrowser, }, - async function taskFn(browser) { + async function taskFn() { const localeNotificationBar = content.document.getElementById( "locale-notification" ); @@ -1107,7 +1104,7 @@ add_task(async function testLocaleGating() { url: "about:ion", gBrowser, }, - async function taskFn(browser) { + async function taskFn() { const localeNotificationBar = content.document.getElementById( "locale-notification" ); diff --git a/browser/components/messagepreview/messagepreview.js b/browser/components/messagepreview/messagepreview.js index 48e5fb1ff5..bec0a2d8eb 100644 --- a/browser/components/messagepreview/messagepreview.js +++ b/browser/components/messagepreview/messagepreview.js @@ -6,13 +6,25 @@ "use strict"; +// decode a 16-bit string in which only one byte of each +// 16-bit unit is occupied, to UTF-8. This is necessary to +// comply with `btoa` API constraints. +function fromBinary(encoded) { + const binary = atob(decodeURIComponent(encoded)); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return String.fromCharCode(...new Uint16Array(bytes.buffer)); +} + function decodeMessageFromUrl() { const url = new URL(document.location.href); if (url.searchParams.has("json")) { const encodedMessage = url.searchParams.get("json"); - return atob(encodedMessage); + return fromBinary(encodedMessage); } return null; } diff --git a/browser/components/migration/.eslintrc.js b/browser/components/migration/.eslintrc.js index 34d8ceec2d..41a71782f1 100644 --- a/browser/components/migration/.eslintrc.js +++ b/browser/components/migration/.eslintrc.js @@ -5,7 +5,6 @@ "use strict"; module.exports = { - extends: ["plugin:mozilla/require-jsdoc"], rules: { "block-scoped-var": "error", complexity: ["error", { max: 22 }], @@ -14,7 +13,7 @@ module.exports = { "no-multi-str": "error", "no-return-assign": "error", "no-shadow": "error", - "no-unused-vars": ["error", { args: "after-used", vars: "all" }], + "no-unused-vars": ["error", { argsIgnorePattern: "^_", vars: "all" }], strict: ["error", "global"], yoda: "error", }, @@ -26,7 +25,7 @@ module.exports = { "no-unused-vars": [ "error", { - args: "none", + argsIgnorePattern: "^_", vars: "local", }, ], diff --git a/browser/components/migration/FileMigrators.sys.mjs b/browser/components/migration/FileMigrators.sys.mjs index 3384011c13..487d77aa6c 100644 --- a/browser/components/migration/FileMigrators.sys.mjs +++ b/browser/components/migration/FileMigrators.sys.mjs @@ -138,11 +138,10 @@ export class FileMigratorBase { * from the native file picker. This will not be called if the user * chooses to cancel the native file picker. * - * @param {string} filePath + * @param {string} _filePath * The path that the user selected from the native file picker. */ - // eslint-disable-next-line no-unused-vars - async migrate(filePath) { + async migrate(_filePath) { throw new Error("FileMigrator.migrate must be overridden."); } } diff --git a/browser/components/migration/MigratorBase.sys.mjs b/browser/components/migration/MigratorBase.sys.mjs index 52bfc87b3e..32bed4e6ec 100644 --- a/browser/components/migration/MigratorBase.sys.mjs +++ b/browser/components/migration/MigratorBase.sys.mjs @@ -141,7 +141,7 @@ export class MigratorBase { * bookmarks file exists. * * @abstract - * @param {object|string} aProfile + * @param {object|string} _aProfile * The profile from which data may be imported, or an empty string * in the case of a single-profile migrator. * In the case of multiple-profiles migrator, it is guaranteed that @@ -149,8 +149,7 @@ export class MigratorBase { * above). * @returns {Promise<MigratorResource[]>|MigratorResource[]} */ - // eslint-disable-next-line no-unused-vars - getResources(aProfile) { + getResources(_aProfile) { throw new Error("getResources must be overridden"); } @@ -223,14 +222,13 @@ export class MigratorBase { * to getPermissions resolves to true, that the MigratorBase will be able to * get read access to all of the resources it needs to do a migration. * - * @param {DOMWindow} win + * @param {DOMWindow} _win * The top-level DOM window hosting the UI that is requesting the permission. * This can be used to, for example, anchor a file picker window to the * same window that is hosting the migration UI. * @returns {Promise<boolean>} */ - // eslint-disable-next-line no-unused-vars - async getPermissions(win) { + async getPermissions(_win) { return Promise.resolve(true); } diff --git a/browser/components/migration/content/migration-wizard.mjs b/browser/components/migration/content/migration-wizard.mjs index 6fc7a715d7..89872a1558 100644 --- a/browser/components/migration/content/migration-wizard.mjs +++ b/browser/components/migration/content/migration-wizard.mjs @@ -583,9 +583,14 @@ export class MigrationWizard extends HTMLElement { "div[name='page-selection']" ); + let header = selectionPage.querySelector(".migration-wizard-header"); + let selectionHeaderString = this.getAttribute("selection-header-string"); + if (this.hasAttribute("selection-header-string")) { - selectionPage.querySelector(".migration-wizard-header").textContent = - this.getAttribute("selection-header-string"); + header.textContent = selectionHeaderString; + header.toggleAttribute("hidden", !selectionHeaderString); + } else { + header.removeAttribute("hidden"); } let selectionSubheaderString = this.getAttribute( diff --git a/browser/components/newtab/.eslintrc.js b/browser/components/newtab/.eslintrc.js index f541cdd988..29114a055a 100644 --- a/browser/components/newtab/.eslintrc.js +++ b/browser/components/newtab/.eslintrc.js @@ -15,11 +15,7 @@ module.exports = { { // TODO: Bug 1773467 - Move these to .mjs or figure out a generic way // to identify these as modules. - files: [ - "content-src/**/*.js", - "test/schemas/**/*.js", - "test/unit/**/*.js", - ], + files: ["test/schemas/**/*.js", "test/unit/**/*.js"], parserOptions: { sourceType: "module", }, @@ -92,8 +88,6 @@ module.exports = { }, ], rules: { - "fetch-options/no-fetch-credentials": "error", - "react/jsx-boolean-value": ["error", "always"], "react/jsx-key": "error", "react/jsx-no-bind": [ diff --git a/browser/components/newtab/common/Actions.sys.mjs b/browser/components/newtab/common/Actions.mjs index df5c9f0c91..7273d80220 100644 --- a/browser/components/newtab/common/Actions.sys.mjs +++ b/browser/components/newtab/common/Actions.mjs @@ -2,6 +2,8 @@ * 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/. */ +// This file is accessed from both content and system scopes. + export const MAIN_MESSAGE_TYPE = "ActivityStream:Main"; export const CONTENT_MESSAGE_TYPE = "ActivityStream:Content"; export const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser"; @@ -158,6 +160,7 @@ for (const type of [ "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", + "WALLPAPERS_SET", "WEBEXT_CLICK", "WEBEXT_DISMISS", ]) { @@ -371,8 +374,11 @@ function DiscoveryStreamLoadedContent( return importContext === UI_CODE ? AlsoToMain(action) : action; } -function SetPref(name, value, importContext = globalImportContext) { - const action = { type: actionTypes.SET_PREF, data: { name, value } }; +function SetPref(prefName, value, importContext = globalImportContext) { + const action = { + type: actionTypes.SET_PREF, + data: { name: prefName, value }, + }; return importContext === UI_CODE ? AlsoToMain(action) : action; } diff --git a/browser/components/newtab/common/Reducers.sys.mjs b/browser/components/newtab/common/Reducers.sys.mjs index d4f879b834..326217538d 100644 --- a/browser/components/newtab/common/Reducers.sys.mjs +++ b/browser/components/newtab/common/Reducers.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs"; export const TOP_SITES_DEFAULT_ROWS = 1; @@ -101,6 +101,9 @@ export const INITIAL_STATE = { // Hide the search box after handing off to AwesomeBar and user starts typing. hide: false, }, + Wallpapers: { + wallpaperList: [], + }, }; function App(prevState = INITIAL_STATE.App, action) { @@ -841,6 +844,15 @@ function Search(prevState = INITIAL_STATE.Search, action) { } } +function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { + switch (action.type) { + case at.WALLPAPERS_SET: + return { wallpaperList: action.data }; + default: + return prevState; + } +} + export const reducers = { TopSites, App, @@ -852,4 +864,5 @@ export const reducers = { Personalization, DiscoveryStream, Search, + Wallpapers, }; diff --git a/browser/components/newtab/content-src/activity-stream.jsx b/browser/components/newtab/content-src/activity-stream.jsx index c588e8e850..57ba9f9c92 100644 --- a/browser/components/newtab/content-src/activity-stream.jsx +++ b/browser/components/newtab/content-src/activity-stream.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { Base } from "content-src/components/Base/Base"; import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start"; import { initStore } from "content-src/lib/init-store"; diff --git a/browser/components/newtab/content-src/components/Base/Base.jsx b/browser/components/newtab/content-src/components/Base/Base.jsx index 20402b09f5..1738f8f51a 100644 --- a/browser/components/newtab/content-src/components/Base/Base.jsx +++ b/browser/components/newtab/content-src/components/Base/Base.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DiscoveryStreamAdmin } from "content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin"; import { ConfirmDialog } from "content-src/components/ConfirmDialog/ConfirmDialog"; import { connect } from "react-redux"; @@ -16,6 +13,9 @@ import React from "react"; import { Search } from "content-src/components/Search/Search"; import { Sections } from "content-src/components/Sections/Sections"; +const VISIBLE = "visible"; +const VISIBILITY_CHANGE_EVENT = "visibilitychange"; + export const PrefsButton = ({ onClick, icon }) => ( <div className="prefs-button"> <button @@ -76,7 +76,7 @@ export class _Base extends React.PureComponent { ] .filter(v => v) .join(" "); - global.document.body.className = bodyClassName; + globalThis.document.body.className = bodyClassName; } render() { @@ -110,17 +110,75 @@ export class BaseContent extends React.PureComponent { this.handleOnKeyDown = this.handleOnKeyDown.bind(this); this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5); this.setPref = this.setPref.bind(this); - this.state = { fixedSearch: false }; + this.updateWallpaper = this.updateWallpaper.bind(this); + this.prefersDarkQuery = null; + this.handleColorModeChange = this.handleColorModeChange.bind(this); + this.state = { + fixedSearch: false, + firstVisibleTimestamp: null, + colorMode: "", + }; + } + + setFirstVisibleTimestamp() { + if (!this.state.firstVisibleTimestamp) { + this.setState({ + firstVisibleTimestamp: Date.now(), + }); + } } componentDidMount() { global.addEventListener("scroll", this.onWindowScroll); global.addEventListener("keydown", this.handleOnKeyDown); + if (this.props.document.visibilityState === VISIBLE) { + this.setFirstVisibleTimestamp(); + } else { + this._onVisibilityChange = () => { + if (this.props.document.visibilityState === VISIBLE) { + this.setFirstVisibleTimestamp(); + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + this._onVisibilityChange = null; + } + }; + this.props.document.addEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + // track change event to dark/light mode + this.prefersDarkQuery = globalThis.matchMedia( + "(prefers-color-scheme: dark)" + ); + + this.prefersDarkQuery.addEventListener( + "change", + this.handleColorModeChange + ); + this.handleColorModeChange(); + } + + handleColorModeChange() { + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.setState({ colorMode }); } componentWillUnmount() { + this.prefersDarkQuery?.removeEventListener( + "change", + this.handleColorModeChange + ); global.removeEventListener("scroll", this.onWindowScroll); global.removeEventListener("keydown", this.handleOnKeyDown); + if (this._onVisibilityChange) { + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } } onWindowScroll() { @@ -160,11 +218,79 @@ export class BaseContent extends React.PureComponent { this.props.dispatch(ac.SetPref(pref, value)); } + renderWallpaperAttribution() { + const { wallpaperList } = this.props.Wallpapers; + const activeWallpaper = + this.props.Prefs.values[ + `newtabWallpapers.wallpaper-${this.state.colorMode}` + ]; + const selected = wallpaperList.find(wp => wp.title === activeWallpaper); + // make sure a wallpaper is selected and that the attribution also exists + if (!selected?.attribution) { + return null; + } + + const { name, webpage } = selected.attribution; + if (activeWallpaper && wallpaperList && name.url) { + return ( + <p + className={`wallpaper-attribution`} + key={name} + data-l10n-id="newtab-wallpaper-attribution" + data-l10n-args={JSON.stringify({ + author_string: name.string, + author_url: name.url, + webpage_string: webpage.string, + webpage_url: webpage.url, + })} + > + <a data-l10n-name="name-link" href={name.url}> + {name.string} + </a> + <a data-l10n-name="webpage-link" href={webpage.url}> + {webpage.string} + </a> + </p> + ); + } + return null; + } + + async updateWallpaper() { + const prefs = this.props.Prefs.values; + const { wallpaperList } = this.props.Wallpapers; + + if (wallpaperList) { + const lightWallpaper = + wallpaperList.find( + wp => wp.title === prefs["newtabWallpapers.wallpaper-light"] + ) || ""; + const darkWallpaper = + wallpaperList.find( + wp => wp.title === prefs["newtabWallpapers.wallpaper-dark"] + ) || ""; + global.document?.body.style.setProperty( + `--newtab-wallpaper-light`, + `url(${lightWallpaper?.wallpaperUrl || ""})` + ); + + global.document?.body.style.setProperty( + `--newtab-wallpaper-dark`, + `url(${darkWallpaper?.wallpaperUrl || ""})` + ); + } + } + render() { const { props } = this; const { App } = props; const { initialized, customizeMenuVisible } = App; const prefs = props.Prefs.values; + + const activeWallpaper = + prefs[`newtabWallpapers.wallpaper-${this.state.colorMode}`]; + const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; + const { pocketConfig } = prefs; const isDiscoveryStream = @@ -215,6 +341,9 @@ export class BaseContent extends React.PureComponent { ] .filter(v => v) .join(" "); + if (wallpapersEnabled) { + this.updateWallpaper(); + } return ( <div> @@ -224,6 +353,8 @@ export class BaseContent extends React.PureComponent { openPreferences={this.openPreferences} setPref={this.setPref} enabledSections={enabledSections} + wallpapersEnabled={wallpapersEnabled} + activeWallpaper={activeWallpaper} pocketRegion={pocketRegion} mayHaveSponsoredTopSites={mayHaveSponsoredTopSites} mayHaveSponsoredStories={mayHaveSponsoredStories} @@ -252,6 +383,7 @@ export class BaseContent extends React.PureComponent { <DiscoveryStreamBase locale={props.App.locale} mayHaveSponsoredStories={mayHaveSponsoredStories} + firstVisibleTimestamp={this.state.firstVisibleTimestamp} /> </ErrorBoundary> ) : ( @@ -259,6 +391,7 @@ export class BaseContent extends React.PureComponent { )} </div> <ConfirmDialog /> + {wallpapersEnabled && this.renderWallpaperAttribution()} </main> </div> </div> @@ -266,10 +399,15 @@ export class BaseContent extends React.PureComponent { } } +BaseContent.defaultProps = { + document: global.document, +}; + export const Base = connect(state => ({ App: state.App, Prefs: state.Prefs, Sections: state.Sections, DiscoveryStream: state.DiscoveryStream, Search: state.Search, + Wallpapers: state.Wallpapers, }))(_Base); diff --git a/browser/components/newtab/content-src/components/Base/_Base.scss b/browser/components/newtab/content-src/components/Base/_Base.scss index 1282173df5..a9141e0923 100644 --- a/browser/components/newtab/content-src/components/Base/_Base.scss +++ b/browser/components/newtab/content-src/components/Base/_Base.scss @@ -24,10 +24,17 @@ } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: $wrapper-default-width; padding: 0; + .vertical-center-wrapper { + margin: auto 0; + } + section { margin-bottom: $section-spacing; position: relative; @@ -124,3 +131,32 @@ main { } } } + +.wallpaper-attribution { + padding: 0 $section-horizontal-padding; + font-size: 14px; + + &.theme-light { + display: inline-block; + + @include dark-theme-only { + display: none; + } + } + + &.theme-dark { + display: none; + + @include dark-theme-only { + display: inline-block; + } + } + + a { + color: var(--newtab-element-color); + + &:hover { + text-decoration: none; + } + } +} diff --git a/browser/components/newtab/content-src/components/Card/Card.jsx b/browser/components/newtab/content-src/components/Card/Card.jsx index 9d03377f1b..da5e0346d7 100644 --- a/browser/components/newtab/content-src/components/Card/Card.jsx +++ b/browser/components/newtab/content-src/components/Card/Card.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { cardContextTypes } from "./types"; import { connect } from "react-redux"; import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton"; diff --git a/browser/components/newtab/content-src/components/Card/types.js b/browser/components/newtab/content-src/components/Card/types.mjs index 0b17eea408..0b17eea408 100644 --- a/browser/components/newtab/content-src/components/Card/types.js +++ b/browser/components/newtab/content-src/components/Card/types.mjs diff --git a/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx index 98bf88fbea..2046617ad6 100644 --- a/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx +++ b/browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx @@ -119,7 +119,7 @@ export class _CollapsibleSection extends React.PureComponent { } _CollapsibleSection.defaultProps = { - document: global.document || { + document: globalThis.document || { addEventListener: () => {}, removeEventListener: () => {}, visibilityState: "hidden", diff --git a/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx b/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx index 4efd8c712e..ffcc6b62f4 100644 --- a/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx +++ b/browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { perfService as perfSvc } from "content-src/lib/perf-service"; import React from "react"; diff --git a/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx b/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx index f69e540079..734f261b27 100644 --- a/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx +++ b/browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac, actionTypes } from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes } from "common/Actions.mjs"; import { connect } from "react-redux"; import React from "react"; diff --git a/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx index 5ea6a57f71..458f65e644 100644 --- a/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx +++ b/browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx @@ -26,12 +26,12 @@ export class ContextMenu extends React.PureComponent { componentDidMount() { this.onShow(); setTimeout(() => { - global.addEventListener("click", this.hideContext); + globalThis.addEventListener("click", this.hideContext); }, 0); } componentWillUnmount() { - global.removeEventListener("click", this.hideContext); + globalThis.removeEventListener("click", this.hideContext); } onClick(event) { diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx index 298dedcee5..1dd13fc965 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx @@ -3,8 +3,9 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from "react"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { SafeAnchor } from "../../DiscoveryStreamComponents/SafeAnchor/SafeAnchor"; +import { WallpapersSection } from "../../WallpapersSection/WallpapersSection"; export class ContentSection extends React.PureComponent { constructor(props) { @@ -98,6 +99,9 @@ export class ContentSection extends React.PureComponent { mayHaveRecentSaves, openPreferences, spocMessageVariant, + wallpapersEnabled, + activeWallpaper, + setPref, } = this.props; const { topSitesEnabled, @@ -111,6 +115,15 @@ export class ContentSection extends React.PureComponent { return ( <div className="home-section"> + {wallpapersEnabled && ( + <div className="wallpapers-section"> + <h2 data-l10n-id="newtab-wallpaper-title"></h2> + <WallpapersSection + setPref={setPref} + activeWallpaper={activeWallpaper} + /> + </div> + )} <div id="shortcuts-section" className="section"> <moz-toggle id="shortcuts-toggle" diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx index 54dcd550c4..f1c723fed2 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx +++ b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx @@ -2,7 +2,6 @@ * 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 { BackgroundsSection } from "content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection"; import { ContentSection } from "content-src/components/CustomizeMenu/ContentSection/ContentSection"; import { connect } from "react-redux"; import React from "react"; @@ -62,11 +61,12 @@ export class _CustomizeMenu extends React.PureComponent { data-l10n-id="newtab-custom-close-button" ref={c => (this.closeButton = c)} /> - <BackgroundsSection /> <ContentSection openPreferences={this.props.openPreferences} setPref={this.props.setPref} enabledSections={this.props.enabledSections} + wallpapersEnabled={this.props.wallpapersEnabled} + activeWallpaper={this.props.activeWallpaper} pocketRegion={this.props.pocketRegion} mayHaveSponsoredTopSites={this.props.mayHaveSponsoredTopSites} mayHaveSponsoredStories={this.props.mayHaveSponsoredStories} diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss index 579e455a3f..c20da5ce50 100644 --- a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss +++ b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss @@ -119,6 +119,10 @@ grid-row-gap: 32px; padding: 0 16px; + .wallpapers-section h2 { + font-size: inherit; + } + .section { moz-toggle { margin-bottom: 10px; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx index 3c31a5a29f..8b9d64dfc1 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { connect } from "react-redux"; import React from "react"; import { SimpleHashRouter } from "./SimpleHashRouter"; @@ -445,9 +442,9 @@ export class CollapseToggle extends React.PureComponent { setBodyClass() { if (this.renderAdmin && !this.state.collapsed) { - global.document.body.classList.add("no-scroll"); + globalThis.document.body.classList.add("no-scroll"); } else { - global.document.body.classList.remove("no-scroll"); + globalThis.document.body.classList.remove("no-scroll"); } } @@ -460,7 +457,7 @@ export class CollapseToggle extends React.PureComponent { } componentWillUnmount() { - global.document.body.classList.remove("no-scroll"); + globalThis.document.body.classList.remove("no-scroll"); } render() { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx index 9c3fd8579c..bc7b0c42c5 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx @@ -8,19 +8,19 @@ export class SimpleHashRouter extends React.PureComponent { constructor(props) { super(props); this.onHashChange = this.onHashChange.bind(this); - this.state = { hash: global.location.hash }; + this.state = { hash: globalThis.location.hash }; } onHashChange() { - this.setState({ hash: global.location.hash }); + this.setState({ hash: globalThis.location.hash }); } componentWillMount() { - global.addEventListener("hashchange", this.onHashChange); + globalThis.addEventListener("hashchange", this.onHashChange); } componentWillUnmount() { - global.removeEventListener("hashchange", this.onHashChange); + globalThis.removeEventListener("hashchange", this.onHashChange); } render() { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx index 0f0ee51ab9..8b5826dd82 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx @@ -164,7 +164,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { privacyNoticeURL={component.properties.privacyNoticeURL} /> ); - case "CollectionCardGrid": + case "CollectionCardGrid": { const { DiscoveryStream } = this.props; return ( <CollectionCardGrid @@ -178,6 +178,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { dispatch={this.props.dispatch} /> ); + } case "CardGrid": return ( <CardGrid @@ -200,6 +201,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { editorsPicksHeader={component.properties.editorsPicksHeader} recentSavesEnabled={this.props.DiscoveryStream.recentSavesEnabled} hideDescriptions={this.props.DiscoveryStream.hideDescriptions} + firstVisibleTimestamp={this.props.firstVisibleTimestamp} /> ); case "HorizontalRule": @@ -384,6 +386,6 @@ export const DiscoveryStreamBase = connect(state => ({ DiscoveryStream: state.DiscoveryStream, Prefs: state.Prefs, Sections: state.Sections, - document: global.document, + document: globalThis.document, App: state.App, }))(_DiscoveryStreamBase); diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx index cf00361df2..2a9497d1b4 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx @@ -8,10 +8,7 @@ import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDi import { TopicsWidget } from "../TopicsWidget/TopicsWidget.jsx"; import { SafeAnchor } from "../SafeAnchor/SafeAnchor"; import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React, { useEffect, useState, useRef, useCallback } from "react"; import { connect, useSelector } from "react-redux"; const PREF_ONBOARDING_EXPERIENCE_DISMISSED = @@ -31,7 +28,7 @@ export function DSSubHeader({ children }) { ); } -export function OnboardingExperience({ dispatch, windowObj = global }) { +export function OnboardingExperience({ dispatch, windowObj = globalThis }) { const [dismissed, setDismissed] = useState(false); const [maxHeight, setMaxHeight] = useState(null); const heightElement = useRef(null); @@ -361,6 +358,7 @@ export class _CardGrid extends React.PureComponent { url={rec.url} id={rec.id} shim={rec.shim} + fetchTimestamp={rec.fetchTimestamp} type={this.props.type} context={rec.context} sponsor={rec.sponsor} @@ -377,6 +375,7 @@ export class _CardGrid extends React.PureComponent { ctaButtonVariant={ctaButtonVariant} spocMessageVariant={spocMessageVariant} recommendation_id={rec.recommendation_id} + firstVisibleTimestamp={this.props.firstVisibleTimestamp} /> ) ); diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx index d089a5c8ab..4f3f150a9b 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { CardGrid } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid"; import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss"; import { LinkMenuOptions } from "content-src/lib/link-menu-options"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx index f3e1eab503..b3d965530d 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DSImage } from "../DSImage/DSImage.jsx"; import { DSLinkMenu } from "../DSLinkMenu/DSLinkMenu"; import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats"; @@ -198,6 +195,8 @@ export class _DSCard extends React.PureComponent { ...(this.props.shim && this.props.shim.click ? { shim: this.props.shim.click } : {}), + fetchTimestamp: this.props.fetchTimestamp, + firstVisibleTimestamp: this.props.firstVisibleTimestamp, }, }) ); @@ -245,6 +244,8 @@ export class _DSCard extends React.PureComponent { ...(this.props.shim && this.props.shim.save ? { shim: this.props.shim.save } : {}), + fetchTimestamp: this.props.fetchTimestamp, + firstVisibleTimestamp: this.props.firstVisibleTimestamp, }, }) ); @@ -441,10 +442,12 @@ export class _DSCard extends React.PureComponent { ? { shim: this.props.shim.impression } : {}), recommendation_id: this.props.recommendation_id, + fetchTimestamp: this.props.fetchTimestamp, }, ]} dispatch={this.props.dispatch} source={this.props.type} + firstVisibleTimestamp={this.props.firstVisibleTimestamp} /> </SafeAnchor> {ctaButtonVariant === "variant-b" && ( diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx index 6c0641cfc1..80af05c585 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx @@ -2,7 +2,7 @@ * 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 { cardContextTypes } from "../../Card/types.js"; +import { cardContextTypes } from "../../Card/types.mjs"; import { SponsoredContentHighlight } from "../FeatureHighlight/SponsoredContentHighlight"; import { CSSTransition, TransitionGroup } from "react-transition-group"; import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx index ff3886b407..ed90f68606 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React from "react"; export class DSEmptyState extends React.PureComponent { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx index b75063940c..107adca4da 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx @@ -4,7 +4,7 @@ import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import React from "react"; export class DSLinkMenu extends React.PureComponent { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx index b251fb0401..2275f8b22b 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx @@ -3,10 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from "react"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { ModalOverlayWrapper } from "content-src/components/ModalOverlay/ModalOverlay"; export class DSPrivacyModal extends React.PureComponent { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx index b7e3205646..0a4d687c65 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton"; import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx index 02a3326eb7..fc52decdf8 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss"; import { DSImage } from "../DSImage/DSImage.jsx"; import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx index 792be40ba3..c650453393 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React, { useState, useCallback, useRef, useEffect } from "react"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; export function FeatureHighlight({ message, diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx index 1062c3cade..43865c177c 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import React from "react"; import { SafeAnchor } from "../SafeAnchor/SafeAnchor"; import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx index 72ec94e1fe..b586730713 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React from "react"; export class SafeAnchor extends React.PureComponent { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx index 1fe2343b94..59b44198a2 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import React from "react"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { SafeAnchor } from "../SafeAnchor/SafeAnchor"; import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats"; import { connect } from "react-redux"; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx index 1eb4863271..9342fcd27a 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { TOP_SITES_SOURCE } from "../TopSites/TopSitesConstants"; import React from "react"; @@ -100,7 +97,9 @@ export class ImpressionStats extends React.PureComponent { type: this.props.flightId ? "spoc" : "organic", ...(link.shim ? { shim: link.shim } : {}), recommendation_id: link.recommendation_id, + fetchTimestamp: link.fetchTimestamp, })), + firstVisibleTimestamp: this.props.firstVisibleTimestamp, }) ); this.impressionCardGuids = cards.map(link => link.id); @@ -244,8 +243,8 @@ export class ImpressionStats extends React.PureComponent { } ImpressionStats.defaultProps = { - IntersectionObserver: global.IntersectionObserver, - document: global.document, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, rows: [], source: "", }; diff --git a/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx b/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx index 650a03eb95..65b1f38623 100644 --- a/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx +++ b/browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { connect } from "react-redux"; import { ContextMenu } from "content-src/components/ContextMenu/ContextMenu"; import { LinkMenuOptions } from "content-src/lib/link-menu-options"; diff --git a/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx b/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx index fdfdf22db2..5d902b43ba 100644 --- a/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx +++ b/browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx @@ -53,4 +53,4 @@ export class ModalOverlayWrapper extends React.PureComponent { } } -ModalOverlayWrapper.defaultProps = { document: global.document }; +ModalOverlayWrapper.defaultProps = { document: globalThis.document }; diff --git a/browser/components/newtab/content-src/components/Search/Search.jsx b/browser/components/newtab/content-src/components/Search/Search.jsx index 64308963c9..ef7a3757d3 100644 --- a/browser/components/newtab/content-src/components/Search/Search.jsx +++ b/browser/components/newtab/content-src/components/Search/Search.jsx @@ -4,10 +4,7 @@ /* globals ContentSearchUIController, ContentSearchHandoffUIController */ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { connect } from "react-redux"; import { IS_NEWTAB } from "content-src/lib/constants"; import React from "react"; diff --git a/browser/components/newtab/content-src/components/Sections/Sections.jsx b/browser/components/newtab/content-src/components/Sections/Sections.jsx index e72e9145ad..01b50f6918 100644 --- a/browser/components/newtab/content-src/components/Sections/Sections.jsx +++ b/browser/components/newtab/content-src/components/Sections/Sections.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { Card, PlaceholderCard } from "content-src/components/Card/Card"; import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection"; import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer"; @@ -33,7 +30,7 @@ export class Section extends React.PureComponent { let cardsPerRow = CARDS_PER_ROW_DEFAULT; if ( props.compactCards && - global.matchMedia(`(min-width: 1072px)`).matches + globalThis.matchMedia(`(min-width: 1072px)`).matches ) { // If the section has compact cards and the viewport is wide enough, we show // 4 columns instead of 3. @@ -326,7 +323,7 @@ export class Section extends React.PureComponent { } Section.defaultProps = { - document: global.document, + document: globalThis.document, rows: [], emptyState: {}, pref: {}, diff --git a/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx b/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx index 4324c019f6..2d504c52ab 100644 --- a/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx +++ b/browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React from "react"; import { TOP_SITES_SOURCE } from "./TopSitesConstants"; diff --git a/browser/components/newtab/content-src/components/TopSites/TopSite.jsx b/browser/components/newtab/content-src/components/TopSites/TopSite.jsx index c0932104af..3d63398e0e 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSite.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSite.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { MIN_RICH_FAVICON_SIZE, MIN_SMALL_FAVICON_SIZE, diff --git a/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx b/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx index 7dd61bdc93..9ca8991735 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { A11yLinkButton } from "content-src/components/A11yLinkButton/A11yLinkButton"; import React from "react"; import { TOP_SITES_SOURCE } from "./TopSitesConstants"; diff --git a/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx b/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx index 580809dd57..b654a803c7 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx @@ -2,7 +2,7 @@ * 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 { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import React from "react"; const VISIBLE = "visible"; @@ -142,8 +142,8 @@ export class TopSiteImpressionWrapper extends React.PureComponent { } TopSiteImpressionWrapper.defaultProps = { - IntersectionObserver: global.IntersectionObserver, - document: global.document, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, actionType: null, tile: null, }; diff --git a/browser/components/newtab/content-src/components/TopSites/TopSites.jsx b/browser/components/newtab/content-src/components/TopSites/TopSites.jsx index ba7676fd10..d9a12aa97d 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSites.jsx +++ b/browser/components/newtab/content-src/components/TopSites/TopSites.jsx @@ -2,10 +2,7 @@ * 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 { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { MIN_RICH_FAVICON_SIZE, TOP_SITES_SOURCE } from "./TopSitesConstants"; import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection"; import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer"; @@ -93,7 +90,7 @@ export class _TopSites extends React.PureComponent { // We hide 2 sites per row when not in the wide layout. let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW; // $break-point-widest = 1072px (from _variables.scss) - if (!global.matchMedia(`(min-width: 1072px)`).matches) { + if (!globalThis.matchMedia(`(min-width: 1072px)`).matches) { sitesPerRow -= 2; } return this.props.TopSites.rows.slice( diff --git a/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js b/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs index f488896238..f488896238 100644 --- a/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js +++ b/browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs diff --git a/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx new file mode 100644 index 0000000000..0b51a146f5 --- /dev/null +++ b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx @@ -0,0 +1,100 @@ +/* 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 React from "react"; +import { connect } from "react-redux"; + +export class _WallpapersSection extends React.PureComponent { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + this.handleReset = this.handleReset.bind(this); + this.prefersHighContrastQuery = null; + this.prefersDarkQuery = null; + } + + componentDidMount() { + this.prefersDarkQuery = globalThis.matchMedia( + "(prefers-color-scheme: dark)" + ); + } + + handleChange(event) { + const { id } = event.target; + const prefs = this.props.Prefs.values; + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, id); + // bug 1892095 + if ( + prefs["newtabWallpapers.wallpaper-dark"] === "" && + colorMode === "light" + ) { + this.props.setPref( + "newtabWallpapers.wallpaper-dark", + id.replace("light", "dark") + ); + } + + if ( + prefs["newtabWallpapers.wallpaper-light"] === "" && + colorMode === "dark" + ) { + this.props.setPref( + `newtabWallpapers.wallpaper-light`, + id.replace("dark", "light") + ); + } + } + + handleReset() { + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, ""); + } + + render() { + const { wallpaperList } = this.props.Wallpapers; + const { activeWallpaper } = this.props; + return ( + <div> + <fieldset className="wallpaper-list"> + {wallpaperList.map(({ title, theme, fluent_id }) => { + return ( + <> + <input + onChange={this.handleChange} + type="radio" + name={`wallpaper-${title}`} + id={title} + value={title} + checked={title === activeWallpaper} + aria-checked={title === activeWallpaper} + className={`wallpaper-input theme-${theme} ${title}`} + /> + <label + htmlFor={title} + className="sr-only" + data-l10n-id={fluent_id} + > + {fluent_id} + </label> + </> + ); + })} + </fieldset> + <button + className="wallpapers-reset" + onClick={this.handleReset} + data-l10n-id="newtab-wallpaper-reset" + /> + </div> + ); + } +} + +export const WallpapersSection = connect(state => { + return { + Wallpapers: state.Wallpapers, + Prefs: state.Prefs, + }; +})(_WallpapersSection); diff --git a/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss b/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss new file mode 100644 index 0000000000..689661750b --- /dev/null +++ b/browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss @@ -0,0 +1,87 @@ +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; + + .wallpaper-input, + .sr-only { + &.theme-light { + display: inline-block; + + @include dark-theme-only { + display: none; + } + } + + &.theme-dark { + display: none; + + @include dark-theme-only { + display: inline-block; + } + } + } + + .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: $shadow-secondary; + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; + + $wallpapers: dark-landscape, dark-color, dark-mountain, dark-panda, dark-sky, dark-beach, light-beach, light-color, light-landscape, light-mountain, light-panda, light-sky; + + @each $wallpaper in $wallpapers { + &.#{$wallpaper} { + background-image: url('chrome://activity-stream/content/data/content/assets/wallpapers/#{$wallpaper}.avif') + } + } + + &:checked { + outline-color: var(--color-accent-primary-active); + } + + &:focus-visible { + outline-color: var(--newtab-primary-action-background); + } + + &:hover { + filter: brightness(55%); + outline-color: transparent; + } + } + + // visually hide label, but still read by screen readers + .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; + pointer-events: none; + } +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; + + &:hover { + text-decoration: none; + } +} diff --git a/browser/components/newtab/content-src/lib/constants.js b/browser/components/newtab/content-src/lib/constants.mjs index 2c96160b4b..4f07a77e29 100644 --- a/browser/components/newtab/content-src/lib/constants.js +++ b/browser/components/newtab/content-src/lib/constants.mjs @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ export const IS_NEWTAB = - global.document && global.document.documentURI === "about:newtab"; + globalThis.document && globalThis.document.documentURI === "about:newtab"; export const NEWTAB_DARK_THEME = { ntp_background: { r: 42, diff --git a/browser/components/newtab/content-src/lib/detect-user-session-start.js b/browser/components/newtab/content-src/lib/detect-user-session-start.mjs index 43aa388967..d4c36efd4a 100644 --- a/browser/components/newtab/content-src/lib/detect-user-session-start.js +++ b/browser/components/newtab/content-src/lib/detect-user-session-start.mjs @@ -5,8 +5,8 @@ import { actionCreators as ac, actionTypes as at, -} from "common/Actions.sys.mjs"; -import { perfService as perfSvc } from "content-src/lib/perf-service"; +} from "../../common/Actions.mjs"; +import { perfService as perfSvc } from "./perf-service.mjs"; const VISIBLE = "visible"; const VISIBILITY_CHANGE_EVENT = "visibilitychange"; @@ -15,7 +15,7 @@ export class DetectUserSessionStart { constructor(store, options = {}) { this._store = store; // Overrides for testing - this.document = options.document || global.document; + this.document = options.document || globalThis.document; this._perfService = options.perfService || perfSvc; this._onVisibilityChange = this._onVisibilityChange.bind(this); } diff --git a/browser/components/newtab/content-src/lib/init-store.js b/browser/components/newtab/content-src/lib/init-store.mjs index f0ab2db86a..85b3b0b470 100644 --- a/browser/components/newtab/content-src/lib/init-store.js +++ b/browser/components/newtab/content-src/lib/init-store.mjs @@ -8,7 +8,10 @@ import { actionCreators as ac, actionTypes as at, actionUtils as au, -} from "common/Actions.sys.mjs"; +} from "../../common/Actions.mjs"; +// We disable import checking here as redux is installed via the npm packages +// at the newtab level, rather than in the top-level package.json. +// eslint-disable-next-line import/no-unresolved import { applyMiddleware, combineReducers, createStore } from "redux"; export const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE"; @@ -117,12 +120,12 @@ export function initStore(reducers, initialState) { const store = createStore( mergeStateReducer(combineReducers(reducers)), initialState, - global.RPMAddMessageListener && + globalThis.RPMAddMessageListener && applyMiddleware(rehydrationMiddleware, messageMiddleware) ); - if (global.RPMAddMessageListener) { - global.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { + if (globalThis.RPMAddMessageListener) { + globalThis.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { try { store.dispatch(msg.data); } catch (ex) { diff --git a/browser/components/newtab/content-src/lib/link-menu-options.js b/browser/components/newtab/content-src/lib/link-menu-options.mjs index 12e47259c1..f10a5e34c6 100644 --- a/browser/components/newtab/content-src/lib/link-menu-options.js +++ b/browser/components/newtab/content-src/lib/link-menu-options.mjs @@ -5,7 +5,7 @@ import { actionCreators as ac, actionTypes as at, -} from "common/Actions.sys.mjs"; +} from "../../common/Actions.mjs"; const _OpenInPrivateWindow = site => ({ id: "newtab-menu-open-new-private-window", diff --git a/browser/components/newtab/content-src/lib/perf-service.js b/browser/components/newtab/content-src/lib/perf-service.mjs index 6ea99ce877..25fc430726 100644 --- a/browser/components/newtab/content-src/lib/perf-service.js +++ b/browser/components/newtab/content-src/lib/perf-service.mjs @@ -2,8 +2,6 @@ * 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/. */ -"use strict"; - let usablePerfObj = window.performance; export function _PerfService(options) { @@ -37,8 +35,8 @@ _PerfService.prototype = { * @param {String} type eg "mark" * @return {Array} Performance* objects */ - getEntriesByName: function getEntriesByName(name, type) { - return this._perf.getEntriesByName(name, type); + getEntriesByName: function getEntriesByName(entryName, type) { + return this._perf.getEntriesByName(entryName, type); }, /** @@ -89,11 +87,11 @@ _PerfService.prototype = { * See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303) * for more info. */ - getMostRecentAbsMarkStartByName(name) { - let entries = this.getEntriesByName(name, "mark"); + getMostRecentAbsMarkStartByName(entryName) { + let entries = this.getEntriesByName(entryName, "mark"); if (!entries.length) { - throw new Error(`No marks with the name ${name}`); + throw new Error(`No marks with the name ${entryName}`); } let mostRecentEntry = entries[entries.length - 1]; diff --git a/browser/components/newtab/content-src/lib/screenshot-utils.js b/browser/components/newtab/content-src/lib/screenshot-utils.mjs index 7ea93f12ae..2d1342be4f 100644 --- a/browser/components/newtab/content-src/lib/screenshot-utils.js +++ b/browser/components/newtab/content-src/lib/screenshot-utils.mjs @@ -30,7 +30,7 @@ export const ScreenshotUtils = { } if (this.isBlob(false, remoteImage)) { return { - url: global.URL.createObjectURL(remoteImage.data), + url: globalThis.URL.createObjectURL(remoteImage.data), path: remoteImage.path, }; } @@ -41,7 +41,7 @@ export const ScreenshotUtils = { // This should always be called with a local image and not a remote image. maybeRevokeBlobObjectURL(localImage) { if (this.isBlob(true, localImage)) { - global.URL.revokeObjectURL(localImage.url); + globalThis.URL.revokeObjectURL(localImage.url); } }, diff --git a/browser/components/newtab/content-src/lib/selectLayoutRender.js b/browser/components/newtab/content-src/lib/selectLayoutRender.mjs index 8ef4dd428f..8ef4dd428f 100644 --- a/browser/components/newtab/content-src/lib/selectLayoutRender.js +++ b/browser/components/newtab/content-src/lib/selectLayoutRender.mjs diff --git a/browser/components/newtab/content-src/styles/_activity-stream.scss b/browser/components/newtab/content-src/styles/_activity-stream.scss index 88ed530b6a..d2e66667b2 100644 --- a/browser/components/newtab/content-src/styles/_activity-stream.scss +++ b/browser/components/newtab/content-src/styles/_activity-stream.scss @@ -21,6 +21,17 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif; font-size: 16px; + + // rules for HNT wallpapers + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; + background-image: var(--newtab-wallpaper-light, ''); + + @media (prefers-color-scheme: dark) { + background-image: var(--newtab-wallpaper-dark, ''); + } } .no-scroll { @@ -137,6 +148,7 @@ input { @import '../components/ContextMenu/ContextMenu'; @import '../components/ConfirmDialog/ConfirmDialog'; @import '../components/CustomizeMenu/CustomizeMenu'; +@import '../components/WallpapersSection/WallpapersSection'; @import '../components/Card/Card'; @import '../components/CollapsibleSection/CollapsibleSection'; @import '../components/DiscoveryStreamAdmin/DiscoveryStreamAdmin'; diff --git a/browser/components/newtab/css/activity-stream-linux.css b/browser/components/newtab/css/activity-stream-linux.css index 8773159737..131ffac535 100644 --- a/browser/components/newtab/css/activity-stream-linux.css +++ b/browser/components/newtab/css/activity-stream-linux.css @@ -276,6 +276,16 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; font-size: 16px; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; + background-image: var(--newtab-wallpaper-light, ""); +} +@media (prefers-color-scheme: dark) { + body { + background-image: var(--newtab-wallpaper-dark, ""); + } } .no-scroll { @@ -405,10 +415,16 @@ input[type=text], input[type=search] { } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: 274px; padding: 0; } +main .vertical-center-wrapper { + margin: auto 0; +} main section { margin-bottom: 20px; position: relative; @@ -489,6 +505,29 @@ main section { background-color: var(--newtab-element-active-color); } +.wallpaper-attribution { + padding: 0 25px; + font-size: 14px; +} +.wallpaper-attribution.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-light { + display: none; +} +.wallpaper-attribution.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark { + display: inline-block; +} +.wallpaper-attribution a { + color: var(--newtab-element-color); +} +.wallpaper-attribution a:hover { + text-decoration: none; +} + .as-error-fallback { align-items: center; border-radius: 3px; @@ -1694,6 +1733,9 @@ main section { grid-row-gap: 32px; padding: 0 16px; } +.home-section .wallpapers-section h2 { + font-size: inherit; +} .home-section .section moz-toggle { margin-bottom: 10px; } @@ -1830,6 +1872,112 @@ main section { box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed); } +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; +} +.wallpaper-list .wallpaper-input.theme-light, +.wallpaper-list .sr-only.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light { + display: none; +} +.wallpaper-list .wallpaper-input.theme-dark, +.wallpaper-list .sr-only.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark { + display: inline-block; +} +.wallpaper-list .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2); + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; +} +.wallpaper-list .wallpaper-input.dark-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif"); +} +.wallpaper-list .wallpaper-input.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.wallpaper-list .wallpaper-input.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.wallpaper-list .wallpaper-input.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.wallpaper-list .wallpaper-input.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.wallpaper-list .wallpaper-input.dark-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.wallpaper-list .wallpaper-input.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.wallpaper-list .wallpaper-input.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.wallpaper-list .wallpaper-input.light-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif"); +} +.wallpaper-list .wallpaper-input.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.wallpaper-list .wallpaper-input:checked { + outline-color: var(--color-accent-primary-active); +} +.wallpaper-list .wallpaper-input:focus-visible { + outline-color: var(--newtab-primary-action-background); +} +.wallpaper-list .wallpaper-input:hover { + filter: brightness(55%); + outline-color: transparent; +} +.wallpaper-list .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; + pointer-events: none; +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; +} +.wallpapers-reset:hover { + text-decoration: none; +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); diff --git a/browser/components/newtab/css/activity-stream-mac.css b/browser/components/newtab/css/activity-stream-mac.css index 87b942818a..416209d511 100644 --- a/browser/components/newtab/css/activity-stream-mac.css +++ b/browser/components/newtab/css/activity-stream-mac.css @@ -280,6 +280,16 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; font-size: 16px; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; + background-image: var(--newtab-wallpaper-light, ""); +} +@media (prefers-color-scheme: dark) { + body { + background-image: var(--newtab-wallpaper-dark, ""); + } } .no-scroll { @@ -409,10 +419,16 @@ input[type=text], input[type=search] { } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: 274px; padding: 0; } +main .vertical-center-wrapper { + margin: auto 0; +} main section { margin-bottom: 20px; position: relative; @@ -493,6 +509,29 @@ main section { background-color: var(--newtab-element-active-color); } +.wallpaper-attribution { + padding: 0 25px; + font-size: 14px; +} +.wallpaper-attribution.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-light { + display: none; +} +.wallpaper-attribution.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark { + display: inline-block; +} +.wallpaper-attribution a { + color: var(--newtab-element-color); +} +.wallpaper-attribution a:hover { + text-decoration: none; +} + .as-error-fallback { align-items: center; border-radius: 3px; @@ -1698,6 +1737,9 @@ main section { grid-row-gap: 32px; padding: 0 16px; } +.home-section .wallpapers-section h2 { + font-size: inherit; +} .home-section .section moz-toggle { margin-bottom: 10px; } @@ -1834,6 +1876,112 @@ main section { box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed); } +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; +} +.wallpaper-list .wallpaper-input.theme-light, +.wallpaper-list .sr-only.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light { + display: none; +} +.wallpaper-list .wallpaper-input.theme-dark, +.wallpaper-list .sr-only.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark { + display: inline-block; +} +.wallpaper-list .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2); + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; +} +.wallpaper-list .wallpaper-input.dark-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif"); +} +.wallpaper-list .wallpaper-input.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.wallpaper-list .wallpaper-input.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.wallpaper-list .wallpaper-input.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.wallpaper-list .wallpaper-input.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.wallpaper-list .wallpaper-input.dark-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.wallpaper-list .wallpaper-input.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.wallpaper-list .wallpaper-input.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.wallpaper-list .wallpaper-input.light-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif"); +} +.wallpaper-list .wallpaper-input.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.wallpaper-list .wallpaper-input:checked { + outline-color: var(--color-accent-primary-active); +} +.wallpaper-list .wallpaper-input:focus-visible { + outline-color: var(--newtab-primary-action-background); +} +.wallpaper-list .wallpaper-input:hover { + filter: brightness(55%); + outline-color: transparent; +} +.wallpaper-list .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; + pointer-events: none; +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; +} +.wallpapers-reset:hover { + text-decoration: none; +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); diff --git a/browser/components/newtab/css/activity-stream-windows.css b/browser/components/newtab/css/activity-stream-windows.css index 25370fdf19..f6118e3c18 100644 --- a/browser/components/newtab/css/activity-stream-windows.css +++ b/browser/components/newtab/css/activity-stream-windows.css @@ -276,6 +276,16 @@ body { background-color: var(--newtab-background-color); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, "Helvetica Neue", sans-serif; font-size: 16px; + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; + background-image: var(--newtab-wallpaper-light, ""); +} +@media (prefers-color-scheme: dark) { + body { + background-image: var(--newtab-wallpaper-dark, ""); + } } .no-scroll { @@ -405,10 +415,16 @@ input[type=text], input[type=search] { } main { - margin: auto; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; width: 274px; padding: 0; } +main .vertical-center-wrapper { + margin: auto 0; +} main section { margin-bottom: 20px; position: relative; @@ -489,6 +505,29 @@ main section { background-color: var(--newtab-element-active-color); } +.wallpaper-attribution { + padding: 0 25px; + font-size: 14px; +} +.wallpaper-attribution.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-light { + display: none; +} +.wallpaper-attribution.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-attribution.theme-dark { + display: inline-block; +} +.wallpaper-attribution a { + color: var(--newtab-element-color); +} +.wallpaper-attribution a:hover { + text-decoration: none; +} + .as-error-fallback { align-items: center; border-radius: 3px; @@ -1694,6 +1733,9 @@ main section { grid-row-gap: 32px; padding: 0 16px; } +.home-section .wallpapers-section h2 { + font-size: inherit; +} .home-section .section moz-toggle { margin-bottom: 10px; } @@ -1830,6 +1872,112 @@ main section { box-shadow: 0 0 0 2px var(--newtab-primary-action-background-dimmed); } +.wallpaper-list { + display: grid; + gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: 86px; + margin: 16px 0; + padding: 0; + border: none; +} +.wallpaper-list .wallpaper-input.theme-light, +.wallpaper-list .sr-only.theme-light { + display: inline-block; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-light, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-light { + display: none; +} +.wallpaper-list .wallpaper-input.theme-dark, +.wallpaper-list .sr-only.theme-dark { + display: none; +} +[lwt-newtab-brighttext] .wallpaper-list .wallpaper-input.theme-dark, +[lwt-newtab-brighttext] .wallpaper-list .sr-only.theme-dark { + display: inline-block; +} +.wallpaper-list .wallpaper-input { + appearance: none; + margin: 0; + padding: 0; + height: 86px; + width: 100%; + box-shadow: 0 1px 4px 0 rgba(12, 12, 13, 0.2); + border-radius: 8px; + background-clip: content-box; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + outline: 2px solid transparent; +} +.wallpaper-list .wallpaper-input.dark-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif"); +} +.wallpaper-list .wallpaper-input.dark-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif"); +} +.wallpaper-list .wallpaper-input.dark-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif"); +} +.wallpaper-list .wallpaper-input.dark-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif"); +} +.wallpaper-list .wallpaper-input.dark-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif"); +} +.wallpaper-list .wallpaper-input.dark-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-beach { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif"); +} +.wallpaper-list .wallpaper-input.light-color { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif"); +} +.wallpaper-list .wallpaper-input.light-landscape { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif"); +} +.wallpaper-list .wallpaper-input.light-mountain { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif"); +} +.wallpaper-list .wallpaper-input.light-panda { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif"); +} +.wallpaper-list .wallpaper-input.light-sky { + background-image: url("chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif"); +} +.wallpaper-list .wallpaper-input:checked { + outline-color: var(--color-accent-primary-active); +} +.wallpaper-list .wallpaper-input:focus-visible { + outline-color: var(--newtab-primary-action-background); +} +.wallpaper-list .wallpaper-input:hover { + filter: brightness(55%); + outline-color: transparent; +} +.wallpaper-list .sr-only { + opacity: 0; + overflow: hidden; + position: absolute; + pointer-events: none; +} + +.wallpapers-reset { + background: none; + border: none; + text-decoration: underline; + margin-inline: auto; + display: block; + font-size: var(--font-size-small); + color: var(--newtab-text-primary-color); + cursor: pointer; +} +.wallpapers-reset:hover { + text-decoration: none; +} + /* stylelint-disable max-nesting-depth */ .card-outer { background: var(--newtab-background-color-secondary); diff --git a/browser/components/newtab/data/content/activity-stream.bundle.js b/browser/components/newtab/data/content/activity-stream.bundle.js index 8904ba87d1..395e8c5bb3 100644 --- a/browser/components/newtab/data/content/activity-stream.bundle.js +++ b/browser/components/newtab/data/content/activity-stream.bundle.js @@ -70,11 +70,13 @@ __webpack_require__.d(__webpack_exports__, { renderWithoutState: () => (/* binding */ renderWithoutState) }); -;// CONCATENATED MODULE: ./common/Actions.sys.mjs +;// CONCATENATED MODULE: ./common/Actions.mjs /* 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/. */ +// This file is accessed from both content and system scopes. + const MAIN_MESSAGE_TYPE = "ActivityStream:Main"; const CONTENT_MESSAGE_TYPE = "ActivityStream:Content"; const PRELOAD_MESSAGE_TYPE = "ActivityStream:PreloadedBrowser"; @@ -231,6 +233,7 @@ for (const type of [ "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", + "WALLPAPERS_SET", "WEBEXT_CLICK", "WEBEXT_DISMISS", ]) { @@ -444,8 +447,11 @@ function DiscoveryStreamLoadedContent( return importContext === UI_CODE ? AlsoToMain(action) : action; } -function SetPref(name, value, importContext = globalImportContext) { - const action = { type: actionTypes.SET_PREF, data: { name, value } }; +function SetPref(prefName, value, importContext = globalImportContext) { + const action = { + type: actionTypes.SET_PREF, + data: { name: prefName, value }, + }; return importContext === UI_CODE ? AlsoToMain(action) : action; } @@ -545,19 +551,19 @@ class SimpleHashRouter extends (external_React_default()).PureComponent { super(props); this.onHashChange = this.onHashChange.bind(this); this.state = { - hash: __webpack_require__.g.location.hash + hash: globalThis.location.hash }; } onHashChange() { this.setState({ - hash: __webpack_require__.g.location.hash + hash: globalThis.location.hash }); } componentWillMount() { - __webpack_require__.g.addEventListener("hashchange", this.onHashChange); + globalThis.addEventListener("hashchange", this.onHashChange); } componentWillUnmount() { - __webpack_require__.g.removeEventListener("hashchange", this.onHashChange); + globalThis.removeEventListener("hashchange", this.onHashChange); } render() { const [, ...routes] = this.state.hash.split("-"); @@ -882,9 +888,9 @@ class CollapseToggle extends (external_React_default()).PureComponent { } setBodyClass() { if (this.renderAdmin && !this.state.collapsed) { - __webpack_require__.g.document.body.classList.add("no-scroll"); + globalThis.document.body.classList.add("no-scroll"); } else { - __webpack_require__.g.document.body.classList.remove("no-scroll"); + globalThis.document.body.classList.remove("no-scroll"); } } componentDidMount() { @@ -894,7 +900,7 @@ class CollapseToggle extends (external_React_default()).PureComponent { this.setBodyClass(); } componentWillUnmount() { - __webpack_require__.g.document.body.classList.remove("no-scroll"); + globalThis.document.body.classList.remove("no-scroll"); } render() { const { @@ -1262,11 +1268,11 @@ class ContextMenu extends (external_React_default()).PureComponent { componentDidMount() { this.onShow(); setTimeout(() => { - __webpack_require__.g.addEventListener("click", this.hideContext); + globalThis.addEventListener("click", this.hideContext); }, 0); } componentWillUnmount() { - __webpack_require__.g.removeEventListener("click", this.hideContext); + globalThis.removeEventListener("click", this.hideContext); } onClick(event) { // Eat all clicks on the context menu so they don't bubble up to window. @@ -1392,23 +1398,21 @@ class _ContextMenuItem extends (external_React_default()).PureComponent { const ContextMenuItem = (0,external_ReactRedux_namespaceObject.connect)(state => ({ Prefs: state.Prefs }))(_ContextMenuItem); -;// CONCATENATED MODULE: ./content-src/lib/link-menu-options.js +;// CONCATENATED MODULE: ./content-src/lib/link-menu-options.mjs /* 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/. */ + const _OpenInPrivateWindow = site => ({ id: "newtab-menu-open-new-private-window", icon: "new-window-private", action: actionCreators.OnlyToMain({ type: actionTypes.OPEN_PRIVATE_WINDOW, - data: { - url: site.url, - referrer: site.referrer - } + data: { url: site.url, referrer: site.referrer }, }), - userEvent: "OPEN_PRIVATE_WINDOW" + userEvent: "OPEN_PRIVATE_WINDOW", }); /** @@ -1417,19 +1421,15 @@ const _OpenInPrivateWindow = site => ({ * the index of the site. */ const LinkMenuOptions = { - Separator: () => ({ - type: "separator" - }), - EmptyItem: () => ({ - type: "empty" - }), + Separator: () => ({ type: "separator" }), + EmptyItem: () => ({ type: "empty" }), ShowPrivacyInfo: () => ({ id: "newtab-menu-show-privacy-info", icon: "info", action: { - type: actionTypes.SHOW_PRIVACY_INFO + type: actionTypes.SHOW_PRIVACY_INFO, }, - userEvent: "SHOW_PRIVACY_INFO" + userEvent: "SHOW_PRIVACY_INFO", }), AboutSponsored: site => ({ id: "newtab-menu-show-privacy-info", @@ -1439,32 +1439,28 @@ const LinkMenuOptions = { data: { advertiser_name: (site.label || site.hostname).toLocaleLowerCase(), position: site.sponsored_position, - tile_id: site.sponsored_tile_id - } + tile_id: site.sponsored_tile_id, + }, }), - userEvent: "TOPSITE_SPONSOR_INFO" + userEvent: "TOPSITE_SPONSOR_INFO", }), RemoveBookmark: site => ({ id: "newtab-menu-remove-bookmark", icon: "bookmark-added", action: actionCreators.AlsoToMain({ type: actionTypes.DELETE_BOOKMARK_BY_ID, - data: site.bookmarkGuid + data: site.bookmarkGuid, }), - userEvent: "BOOKMARK_DELETE" + userEvent: "BOOKMARK_DELETE", }), AddBookmark: site => ({ id: "newtab-menu-bookmark", icon: "bookmark-hollow", action: actionCreators.AlsoToMain({ type: actionTypes.BOOKMARK_URL, - data: { - url: site.url, - title: site.title, - type: site.type - } + data: { url: site.url, title: site.title, type: site.type }, }), - userEvent: "BOOKMARK_ADD" + userEvent: "BOOKMARK_ADD", }), OpenInNewWindow: site => ({ id: "newtab-menu-open-new-window", @@ -1475,10 +1471,10 @@ const LinkMenuOptions = { referrer: site.referrer, typedBonus: site.typedBonus, url: site.url, - sponsored_tile_id: site.sponsored_tile_id - } + sponsored_tile_id: site.sponsored_tile_id, + }, }), - userEvent: "OPEN_NEW_WINDOW" + userEvent: "OPEN_NEW_WINDOW", }), // This blocks the url for regular stories, // but also sends a message to DiscoveryStream with flight_id. @@ -1499,20 +1495,20 @@ const LinkMenuOptions = { pocket_id: site.pocket_id, // used by PlacesFeed and TopSitesFeed for sponsored top sites blocking. isSponsoredTopSite: site.sponsored_position, - ...(site.flight_id ? { - flight_id: site.flight_id - } : {}), + ...(site.flight_id ? { flight_id: site.flight_id } : {}), // If not sponsored, hostname could be anything (Cat3 Data!). // So only put in advertiser_name for sponsored topsites. - ...(site.sponsored_position ? { - advertiser_name: (site.label || site.hostname)?.toLocaleLowerCase() - } : {}), + ...(site.sponsored_position + ? { + advertiser_name: ( + site.label || site.hostname + )?.toLocaleLowerCase(), + } + : {}), position: pos, - ...(site.sponsored_tile_id ? { - tile_id: site.sponsored_tile_id - } : {}), - is_pocket_card: site.type === "CardGrid" - })) + ...(site.sponsored_tile_id ? { tile_id: site.sponsored_tile_id } : {}), + is_pocket_card: site.type === "CardGrid", + })), }), impression: actionCreators.ImpressionStats({ source: eventSource, @@ -1520,13 +1516,12 @@ const LinkMenuOptions = { tiles: tiles.map((site, index) => ({ id: site.guid, pos: pos + index, - ...(site.shim && site.shim.delete ? { - shim: site.shim.delete - } : {}) - })) + ...(site.shim && site.shim.delete ? { shim: site.shim.delete } : {}), + })), }), - userEvent: "BLOCK" + userEvent: "BLOCK", }), + // This is an option for web extentions which will result in remove items from // memory and notify the web extenion, rather than using the built-in block list. WebExtDismiss: (site, index, eventSource) => ({ @@ -1536,8 +1531,8 @@ const LinkMenuOptions = { action: actionCreators.WebExtEvent(actionTypes.WEBEXT_DISMISS, { source: eventSource, url: site.url, - action_position: index - }) + action_position: index, + }), }), DeleteUrl: (site, index, eventSource, isEnabled, siteInfo) => ({ id: "newtab-menu-delete-history", @@ -1545,77 +1540,74 @@ const LinkMenuOptions = { action: { type: actionTypes.DIALOG_OPEN, data: { - onConfirm: [actionCreators.AlsoToMain({ - type: actionTypes.DELETE_HISTORY_URL, - data: { - url: site.url, - pocket_id: site.pocket_id, - forceBlock: site.bookmarkGuid - } - }), actionCreators.UserEvent(Object.assign({ - event: "DELETE", - source: eventSource, - action_position: index - }, siteInfo))], + onConfirm: [ + actionCreators.AlsoToMain({ + type: actionTypes.DELETE_HISTORY_URL, + data: { + url: site.url, + pocket_id: site.pocket_id, + forceBlock: site.bookmarkGuid, + }, + }), + actionCreators.UserEvent( + Object.assign( + { event: "DELETE", source: eventSource, action_position: index }, + siteInfo + ) + ), + ], eventSource, - body_string_id: ["newtab-confirm-delete-history-p1", "newtab-confirm-delete-history-p2"], + body_string_id: [ + "newtab-confirm-delete-history-p1", + "newtab-confirm-delete-history-p2", + ], confirm_button_string_id: "newtab-topsites-delete-history-button", cancel_button_string_id: "newtab-topsites-cancel-button", - icon: "modal-delete" - } + icon: "modal-delete", + }, }, - userEvent: "DIALOG_OPEN" + userEvent: "DIALOG_OPEN", }), ShowFile: site => ({ id: "newtab-menu-show-file", icon: "search", action: actionCreators.OnlyToMain({ type: actionTypes.SHOW_DOWNLOAD_FILE, - data: { - url: site.url - } - }) + data: { url: site.url }, + }), }), OpenFile: site => ({ id: "newtab-menu-open-file", icon: "open-file", action: actionCreators.OnlyToMain({ type: actionTypes.OPEN_DOWNLOAD_FILE, - data: { - url: site.url - } - }) + data: { url: site.url }, + }), }), CopyDownloadLink: site => ({ id: "newtab-menu-copy-download-link", icon: "copy", action: actionCreators.OnlyToMain({ type: actionTypes.COPY_DOWNLOAD_LINK, - data: { - url: site.url - } - }) + data: { url: site.url }, + }), }), GoToDownloadPage: site => ({ id: "newtab-menu-go-to-download-page", icon: "download", action: actionCreators.OnlyToMain({ type: actionTypes.OPEN_LINK, - data: { - url: site.referrer - } + data: { url: site.referrer }, }), - disabled: !site.referrer + disabled: !site.referrer, }), RemoveDownload: site => ({ id: "newtab-menu-remove-download", icon: "delete", action: actionCreators.OnlyToMain({ type: actionTypes.REMOVE_DOWNLOAD_FILE, - data: { - url: site.url - } - }) + data: { url: site.url }, + }), }), PinTopSite: (site, index) => ({ id: "newtab-menu-pin", @@ -1624,23 +1616,19 @@ const LinkMenuOptions = { type: actionTypes.TOP_SITES_PIN, data: { site, - index - } + index, + }, }), - userEvent: "PIN" + userEvent: "PIN", }), UnpinTopSite: site => ({ id: "newtab-menu-unpin", icon: "unpin", action: actionCreators.AlsoToMain({ type: actionTypes.TOP_SITES_UNPIN, - data: { - site: { - url: site.url - } - } + data: { site: { url: site.url } }, }), - userEvent: "UNPIN" + userEvent: "UNPIN", }), SaveToPocket: (site, index, eventSource = "CARDGRID") => ({ id: "newtab-menu-save-to-pocket", @@ -1648,65 +1636,76 @@ const LinkMenuOptions = { action: actionCreators.AlsoToMain({ type: actionTypes.SAVE_TO_POCKET, data: { - site: { - url: site.url, - title: site.title - } - } + site: { url: site.url, title: site.title }, + }, }), impression: actionCreators.ImpressionStats({ source: eventSource, pocket: 0, - tiles: [{ - id: site.guid, - pos: index, - ...(site.shim && site.shim.save ? { - shim: site.shim.save - } : {}) - }] + tiles: [ + { + id: site.guid, + pos: index, + ...(site.shim && site.shim.save ? { shim: site.shim.save } : {}), + }, + ], }), - userEvent: "SAVE_TO_POCKET" + userEvent: "SAVE_TO_POCKET", }), DeleteFromPocket: site => ({ id: "newtab-menu-delete-pocket", icon: "pocket-delete", action: actionCreators.AlsoToMain({ type: actionTypes.DELETE_FROM_POCKET, - data: { - pocket_id: site.pocket_id - } + data: { pocket_id: site.pocket_id }, }), - userEvent: "DELETE_FROM_POCKET" + userEvent: "DELETE_FROM_POCKET", }), ArchiveFromPocket: site => ({ id: "newtab-menu-archive-pocket", icon: "pocket-archive", action: actionCreators.AlsoToMain({ type: actionTypes.ARCHIVE_FROM_POCKET, - data: { - pocket_id: site.pocket_id - } + data: { pocket_id: site.pocket_id }, }), - userEvent: "ARCHIVE_FROM_POCKET" + userEvent: "ARCHIVE_FROM_POCKET", }), EditTopSite: (site, index) => ({ id: "newtab-menu-edit-topsites", icon: "edit", action: { type: actionTypes.TOP_SITES_EDIT, - data: { - index - } - } + data: { index }, + }, }), - CheckBookmark: site => site.bookmarkGuid ? LinkMenuOptions.RemoveBookmark(site) : LinkMenuOptions.AddBookmark(site), - CheckPinTopSite: (site, index) => site.isPinned ? LinkMenuOptions.UnpinTopSite(site) : LinkMenuOptions.PinTopSite(site, index), - CheckSavedToPocket: (site, index, source) => site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.SaveToPocket(site, index, source), - CheckBookmarkOrArchive: site => site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.CheckBookmark(site), - CheckArchiveFromPocket: site => site.pocket_id ? LinkMenuOptions.ArchiveFromPocket(site) : LinkMenuOptions.EmptyItem(), - CheckDeleteFromPocket: site => site.pocket_id ? LinkMenuOptions.DeleteFromPocket(site) : LinkMenuOptions.EmptyItem(), - OpenInPrivateWindow: (site, index, eventSource, isEnabled) => isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem() + CheckBookmark: site => + site.bookmarkGuid + ? LinkMenuOptions.RemoveBookmark(site) + : LinkMenuOptions.AddBookmark(site), + CheckPinTopSite: (site, index) => + site.isPinned + ? LinkMenuOptions.UnpinTopSite(site) + : LinkMenuOptions.PinTopSite(site, index), + CheckSavedToPocket: (site, index, source) => + site.pocket_id + ? LinkMenuOptions.DeleteFromPocket(site) + : LinkMenuOptions.SaveToPocket(site, index, source), + CheckBookmarkOrArchive: site => + site.pocket_id + ? LinkMenuOptions.ArchiveFromPocket(site) + : LinkMenuOptions.CheckBookmark(site), + CheckArchiveFromPocket: site => + site.pocket_id + ? LinkMenuOptions.ArchiveFromPocket(site) + : LinkMenuOptions.EmptyItem(), + CheckDeleteFromPocket: site => + site.pocket_id + ? LinkMenuOptions.DeleteFromPocket(site) + : LinkMenuOptions.EmptyItem(), + OpenInPrivateWindow: (site, index, eventSource, isEnabled) => + isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem(), }; + ;// CONCATENATED MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx /* 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, @@ -1927,21 +1926,47 @@ class DSLinkMenu extends (external_React_default()).PureComponent { }))); } } -;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSitesConstants.js +;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSitesConstants.mjs /* 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/. */ const TOP_SITES_SOURCE = "TOP_SITES"; -const TOP_SITES_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "EditTopSite", "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "DeleteUrl"]; -const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "ShowPrivacyInfo"]; -const TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", "AboutSponsored"]; +const TOP_SITES_CONTEXT_MENU_OPTIONS = [ + "CheckPinTopSite", + "EditTopSite", + "Separator", + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "DeleteUrl", +]; +const TOP_SITES_SPOC_CONTEXT_MENU_OPTIONS = [ + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "ShowPrivacyInfo", +]; +const TOP_SITES_SPONSORED_POSITION_CONTEXT_MENU_OPTIONS = [ + "OpenInNewWindow", + "OpenInPrivateWindow", + "Separator", + "BlockUrl", + "AboutSponsored", +]; // the special top site for search shortcut experiment can only have the option to unpin (which removes) the topsite -const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = ["CheckPinTopSite", "Separator", "BlockUrl"]; +const TOP_SITES_SEARCH_SHORTCUTS_CONTEXT_MENU_OPTIONS = [ + "CheckPinTopSite", + "Separator", + "BlockUrl", +]; // minimum size necessary to show a rich icon instead of a screenshot const MIN_RICH_FAVICON_SIZE = 96; // minimum size necessary to show any icon const MIN_SMALL_FAVICON_SIZE = 16; + ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx /* 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, @@ -2033,8 +2058,10 @@ class ImpressionStats_ImpressionStats extends (external_React_default()).PureCom ...(link.shim ? { shim: link.shim } : {}), - recommendation_id: link.recommendation_id - })) + recommendation_id: link.recommendation_id, + fetchTimestamp: link.fetchTimestamp + })), + firstVisibleTimestamp: this.props.firstVisibleTimestamp })); this.impressionCardGuids = cards.map(link => link.id); } @@ -2146,8 +2173,8 @@ class ImpressionStats_ImpressionStats extends (external_React_default()).PureCom } } ImpressionStats_ImpressionStats.defaultProps = { - IntersectionObserver: __webpack_require__.g.IntersectionObserver, - document: __webpack_require__.g.document, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, rows: [], source: "" }; @@ -2224,7 +2251,7 @@ class SafeAnchor extends (external_React_default()).PureComponent { }, this.props.children); } } -;// CONCATENATED MODULE: ./content-src/components/Card/types.js +;// CONCATENATED MODULE: ./content-src/components/Card/types.mjs /* 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/. */ @@ -2232,29 +2259,30 @@ class SafeAnchor extends (external_React_default()).PureComponent { const cardContextTypes = { history: { fluentID: "newtab-label-visited", - icon: "history-item" + icon: "history-item", }, removedBookmark: { fluentID: "newtab-label-removed-bookmark", - icon: "bookmark-removed" + icon: "bookmark-removed", }, bookmark: { fluentID: "newtab-label-bookmarked", - icon: "bookmark-added" + icon: "bookmark-added", }, trending: { fluentID: "newtab-label-recommended", - icon: "trending" + icon: "trending", }, pocket: { fluentID: "newtab-label-saved", - icon: "pocket" + icon: "pocket", }, download: { fluentID: "newtab-label-download", - icon: "download" - } + icon: "download", + }, }; + ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx /* 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, @@ -2710,7 +2738,9 @@ class _DSCard extends (external_React_default()).PureComponent { tile_id: this.props.id, ...(this.props.shim && this.props.shim.click ? { shim: this.props.shim.click - } : {}) + } : {}), + fetchTimestamp: this.props.fetchTimestamp, + firstVisibleTimestamp: this.props.firstVisibleTimestamp } })); this.props.dispatch(actionCreators.ImpressionStats({ @@ -2751,7 +2781,9 @@ class _DSCard extends (external_React_default()).PureComponent { tile_id: this.props.id, ...(this.props.shim && this.props.shim.save ? { shim: this.props.shim.save - } : {}) + } : {}), + fetchTimestamp: this.props.fetchTimestamp, + firstVisibleTimestamp: this.props.firstVisibleTimestamp } })); this.props.dispatch(actionCreators.ImpressionStats({ @@ -2913,10 +2945,12 @@ class _DSCard extends (external_React_default()).PureComponent { ...(this.props.shim && this.props.shim.impression ? { shim: this.props.shim.impression } : {}), - recommendation_id: this.props.recommendation_id + recommendation_id: this.props.recommendation_id, + fetchTimestamp: this.props.fetchTimestamp }], dispatch: this.props.dispatch, - source: this.props.type + source: this.props.type, + firstVisibleTimestamp: this.props.firstVisibleTimestamp })), ctaButtonVariant === "variant-b" && /*#__PURE__*/external_React_default().createElement("div", { className: "cta-header" }, "Shop Now"), /*#__PURE__*/external_React_default().createElement(DefaultMeta, { @@ -3273,7 +3307,7 @@ function DSSubHeader({ } function OnboardingExperience({ dispatch, - windowObj = __webpack_require__.g + windowObj = globalThis }) { const [dismissed, setDismissed] = (0,external_React_namespaceObject.useState)(false); const [maxHeight, setMaxHeight] = (0,external_React_namespaceObject.useState)(null); @@ -3549,6 +3583,7 @@ class _CardGrid extends (external_React_default()).PureComponent { url: rec.url, id: rec.id, shim: rec.shim, + fetchTimestamp: rec.fetchTimestamp, type: this.props.type, context: rec.context, sponsor: rec.sponsor, @@ -3564,7 +3599,8 @@ class _CardGrid extends (external_React_default()).PureComponent { ctaButtonSponsors: ctaButtonSponsors, ctaButtonVariant: ctaButtonVariant, spocMessageVariant: spocMessageVariant, - recommendation_id: rec.recommendation_id + recommendation_id: rec.recommendation_id, + firstVisibleTimestamp: this.props.firstVisibleTimestamp })); } if (widgets?.positions?.length && widgets?.data?.length) { @@ -4023,7 +4059,7 @@ class _CollapsibleSection extends (external_React_default()).PureComponent { } } _CollapsibleSection.defaultProps = { - document: __webpack_require__.g.document || { + document: globalThis.document || { addEventListener: () => {}, removeEventListener: () => {}, visibilityState: "hidden" @@ -4111,7 +4147,7 @@ class ModalOverlayWrapper extends (external_React_default()).PureComponent { } } ModalOverlayWrapper.defaultProps = { - document: __webpack_require__.g.document + document: globalThis.document }; ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx /* This Source Code Form is subject to the terms of the Mozilla Public @@ -4443,7 +4479,7 @@ class DSTextPromo extends (external_React_default()).PureComponent { }))); } } -;// CONCATENATED MODULE: ./content-src/lib/screenshot-utils.js +;// CONCATENATED MODULE: ./content-src/lib/screenshot-utils.mjs /* 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/. */ @@ -4462,8 +4498,13 @@ class DSTextPromo extends (external_React_default()).PureComponent { */ const ScreenshotUtils = { isBlob(isLocal, image) { - return !!(image && image.path && (!isLocal && image.data || isLocal && image.url)); + return !!( + image && + image.path && + ((!isLocal && image.data) || (isLocal && image.url)) + ); }, + // This should always be called with a remote image and not a local image. createLocalImageObject(remoteImage) { if (!remoteImage) { @@ -4471,33 +4512,36 @@ const ScreenshotUtils = { } if (this.isBlob(false, remoteImage)) { return { - url: __webpack_require__.g.URL.createObjectURL(remoteImage.data), - path: remoteImage.path + url: globalThis.URL.createObjectURL(remoteImage.data), + path: remoteImage.path, }; } - return { - url: remoteImage - }; + return { url: remoteImage }; }, + // Revokes the object URL of the image if the local image is a blob. // This should always be called with a local image and not a remote image. maybeRevokeBlobObjectURL(localImage) { if (this.isBlob(true, localImage)) { - __webpack_require__.g.URL.revokeObjectURL(localImage.url); + globalThis.URL.revokeObjectURL(localImage.url); } }, + // Checks if remoteImage and localImage are the same. isRemoteImageLocal(localImage, remoteImage) { // Both remoteImage and localImage are present. if (remoteImage && localImage) { - return this.isBlob(false, remoteImage) ? localImage.path === remoteImage.path : localImage.url === remoteImage; + return this.isBlob(false, remoteImage) + ? localImage.path === remoteImage.path + : localImage.url === remoteImage; } // This will only handle the remaining three possible outcomes. // (i.e. everything except when both image and localImage are present) return !remoteImage && !localImage; - } + }, }; + ;// CONCATENATED MODULE: ./content-src/components/Card/Card.jsx /* 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, @@ -4822,14 +4866,13 @@ const PlaceholderCard = props => /*#__PURE__*/external_React_default().createEle placeholder: true, className: props.className }); -;// CONCATENATED MODULE: ./content-src/lib/perf-service.js +;// CONCATENATED MODULE: ./content-src/lib/perf-service.mjs /* 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/. */ - - let usablePerfObj = window.performance; + function _PerfService(options) { // For testing, so that we can use a fake Window.performance object with // known state. @@ -4839,6 +4882,7 @@ function _PerfService(options) { this._perf = usablePerfObj; } } + _PerfService.prototype = { /** * Calls the underlying mark() method on the appropriate Window.performance @@ -4851,6 +4895,7 @@ _PerfService.prototype = { mark: function mark(str) { this._perf.mark(str); }, + /** * Calls the underlying getEntriesByName on the appropriate Window.performance * object. @@ -4859,9 +4904,10 @@ _PerfService.prototype = { * @param {String} type eg "mark" * @return {Array} Performance* objects */ - getEntriesByName: function getEntriesByName(name, type) { - return this._perf.getEntriesByName(name, type); + getEntriesByName: function getEntriesByName(entryName, type) { + return this._perf.getEntriesByName(entryName, type); }, + /** * The timeOrigin property from the appropriate performance object. * Used to ensure that timestamps from the add-on code and the content code @@ -4880,6 +4926,7 @@ _PerfService.prototype = { get timeOrigin() { return this._perf.timeOrigin; }, + /** * Returns the "absolute" version of performance.now(), i.e. one that * should ([bug 1401406](https://bugzilla.mozilla.org/show_bug.cgi?id=1401406) @@ -4890,6 +4937,7 @@ _PerfService.prototype = { absNow: function absNow() { return this.timeOrigin + this._perf.now(); }, + /** * This returns the absolute startTime from the most recent performance.mark() * with the given name. @@ -4908,16 +4956,20 @@ _PerfService.prototype = { * See [bug 1369303](https://bugzilla.mozilla.org/show_bug.cgi?id=1369303) * for more info. */ - getMostRecentAbsMarkStartByName(name) { - let entries = this.getEntriesByName(name, "mark"); + getMostRecentAbsMarkStartByName(entryName) { + let entries = this.getEntriesByName(entryName, "mark"); + if (!entries.length) { - throw new Error(`No marks with the name ${name}`); + throw new Error(`No marks with the name ${entryName}`); } + let mostRecentEntry = entries[entries.length - 1]; return this._perf.timeOrigin + mostRecentEntry.startTime; - } + }, }; + const perfService = new _PerfService(); + ;// CONCATENATED MODULE: ./content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx /* 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, @@ -5479,6 +5531,9 @@ const INITIAL_STATE = { // Hide the search box after handing off to AwesomeBar and user starts typing. hide: false, }, + Wallpapers: { + wallpaperList: [], + }, }; function App(prevState = INITIAL_STATE.App, action) { @@ -6219,6 +6274,15 @@ function Search(prevState = INITIAL_STATE.Search, action) { } } +function Wallpapers(prevState = INITIAL_STATE.Wallpapers, action) { + switch (action.type) { + case actionTypes.WALLPAPERS_SET: + return { wallpaperList: action.data }; + default: + return prevState; + } +} + const reducers = { TopSites, App, @@ -6230,6 +6294,7 @@ const reducers = { Personalization: Reducers_sys_Personalization, DiscoveryStream, Search, + Wallpapers, }; ;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx @@ -6448,8 +6513,8 @@ class TopSiteImpressionWrapper extends (external_React_default()).PureComponent } } TopSiteImpressionWrapper.defaultProps = { - IntersectionObserver: __webpack_require__.g.IntersectionObserver, - document: __webpack_require__.g.document, + IntersectionObserver: globalThis.IntersectionObserver, + document: globalThis.document, actionType: null, tile: null }; @@ -7601,7 +7666,7 @@ class _TopSites extends (external_React_default()).PureComponent { // We hide 2 sites per row when not in the wide layout. let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW; // $break-point-widest = 1072px (from _variables.scss) - if (!__webpack_require__.g.matchMedia(`(min-width: 1072px)`).matches) { + if (!globalThis.matchMedia(`(min-width: 1072px)`).matches) { sitesPerRow -= 2; } return this.props.TopSites.rows.slice(0, this.props.TopSitesRows * sitesPerRow); @@ -7733,7 +7798,7 @@ class Section extends (external_React_default()).PureComponent { props } = this; let cardsPerRow = CARDS_PER_ROW_DEFAULT; - if (props.compactCards && __webpack_require__.g.matchMedia(`(min-width: 1072px)`).matches) { + if (props.compactCards && globalThis.matchMedia(`(min-width: 1072px)`).matches) { // If the section has compact cards and the viewport is wide enough, we show // 4 columns instead of 3. // $break-point-widest = 1072px (from _variables.scss) @@ -7969,7 +8034,7 @@ class Section extends (external_React_default()).PureComponent { } } Section.defaultProps = { - document: __webpack_require__.g.document, + document: globalThis.document, rows: [], emptyState: {}, pref: {}, @@ -8188,20 +8253,13 @@ class SectionTitle extends (external_React_default()).PureComponent { }, subtitle) : null); } } -;// CONCATENATED MODULE: ./content-src/lib/selectLayoutRender.js +;// CONCATENATED MODULE: ./content-src/lib/selectLayoutRender.mjs /* 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/. */ -const selectLayoutRender = ({ - state = {}, - prefs = {} -}) => { - const { - layout, - feeds, - spocs - } = state; +const selectLayoutRender = ({ state = {}, prefs = {} }) => { + const { layout, feeds, spocs } = state; let spocIndexPlacementMap = {}; /* This function fills spoc positions on a per placement basis with available spocs. @@ -8210,8 +8268,16 @@ const selectLayoutRender = ({ * If it sees the same placement again, it remembers the previous spoc index, and continues. * If it sees a blocked spoc, it skips that position leaving in a regular story. */ - function fillSpocPositionsForPlacement(data, spocsConfig, spocsData, placementName) { - if (!spocIndexPlacementMap[placementName] && spocIndexPlacementMap[placementName] !== 0) { + function fillSpocPositionsForPlacement( + data, + spocsConfig, + spocsData, + placementName + ) { + if ( + !spocIndexPlacementMap[placementName] && + spocIndexPlacementMap[placementName] !== 0 + ) { spocIndexPlacementMap[placementName] = 0; } const results = [...data]; @@ -8234,107 +8300,154 @@ const selectLayoutRender = ({ results.splice(position.index, 0, spoc); } } + return results; } + const positions = {}; - const DS_COMPONENTS = ["Message", "TextPromo", "SectionTitle", "Signup", "Navigation", "CardGrid", "CollectionCardGrid", "HorizontalRule", "PrivacyLink"]; + const DS_COMPONENTS = [ + "Message", + "TextPromo", + "SectionTitle", + "Signup", + "Navigation", + "CardGrid", + "CollectionCardGrid", + "HorizontalRule", + "PrivacyLink", + ]; + const filterArray = []; + if (!prefs["feeds.topsites"]) { filterArray.push("TopSites"); } - const pocketEnabled = prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"]; + + const pocketEnabled = + prefs["feeds.section.topstories"] && prefs["feeds.system.topstories"]; if (!pocketEnabled) { filterArray.push(...DS_COMPONENTS); } + const placeholderComponent = component => { if (!component.feed) { // TODO we now need a placeholder for topsites and textPromo. return { ...component, data: { - spocs: [] - } + spocs: [], + }, }; } const data = { - recommendations: [] + recommendations: [], }; + let items = 0; if (component.properties && component.properties.items) { items = component.properties.items; } for (let i = 0; i < items; i++) { - data.recommendations.push({ - placeholder: true - }); + data.recommendations.push({ placeholder: true }); } - return { - ...component, - data - }; + + return { ...component, data }; }; // TODO update devtools to show placements const handleSpocs = (data, component) => { let result = [...data]; // Do we ever expect to possibly have a spoc. - if (component.spocs && component.spocs.positions && component.spocs.positions.length) { + if ( + component.spocs && + component.spocs.positions && + component.spocs.positions.length + ) { const placement = component.placement || {}; const placementName = placement.name || "spocs"; const spocsData = spocs.data[placementName]; // We expect a spoc, spocs are loaded, and the server returned spocs. - if (spocs.loaded && spocsData && spocsData.items && spocsData.items.length) { - result = fillSpocPositionsForPlacement(result, component.spocs, spocsData.items, placementName); + if ( + spocs.loaded && + spocsData && + spocsData.items && + spocsData.items.length + ) { + result = fillSpocPositionsForPlacement( + result, + component.spocs, + spocsData.items, + placementName + ); } } return result; }; + const handleComponent = component => { - if (component.spocs && component.spocs.positions && component.spocs.positions.length) { + if ( + component.spocs && + component.spocs.positions && + component.spocs.positions.length + ) { const placement = component.placement || {}; const placementName = placement.name || "spocs"; const spocsData = spocs.data[placementName]; - if (spocs.loaded && spocsData && spocsData.items && spocsData.items.length) { + if ( + spocs.loaded && + spocsData && + spocsData.items && + spocsData.items.length + ) { return { ...component, data: { - spocs: spocsData.items.filter(spoc => spoc && !spocs.blocked.includes(spoc.url)).map((spoc, index) => ({ - ...spoc, - pos: index - })) - } + spocs: spocsData.items + .filter(spoc => spoc && !spocs.blocked.includes(spoc.url)) + .map((spoc, index) => ({ + ...spoc, + pos: index, + })), + }, }; } } return { ...component, data: { - spocs: [] - } + spocs: [], + }, }; }; + const handleComponentWithFeed = component => { positions[component.type] = positions[component.type] || 0; let data = { - recommendations: [] + recommendations: [], }; + const feed = feeds.data[component.feed.url]; if (feed && feed.data) { data = { ...feed.data, - recommendations: [...(feed.data.recommendations || [])] + recommendations: [...(feed.data.recommendations || [])], }; } + if (component && component.properties && component.properties.offset) { data = { ...data, - recommendations: data.recommendations.slice(component.properties.offset) + recommendations: data.recommendations.slice( + component.properties.offset + ), }; } + data = { ...data, - recommendations: handleSpocs(data.recommendations, component) + recommendations: handleSpocs(data.recommendations, component), }; + let items = 0; if (component.properties && component.properties.items) { items = Math.min(component.properties.items, data.recommendations.length); @@ -8346,27 +8459,36 @@ const selectLayoutRender = ({ for (let i = 0; i < items; i++) { data.recommendations[i] = { ...data.recommendations[i], - pos: positions[component.type]++ + pos: positions[component.type]++, }; } - return { - ...component, - data - }; + + return { ...component, data }; }; + const renderLayout = () => { const renderedLayoutArray = []; - for (const row of layout.filter(r => r.components.filter(c => !filterArray.includes(c.type)).length)) { + for (const row of layout.filter( + r => r.components.filter(c => !filterArray.includes(c.type)).length + )) { let components = []; renderedLayoutArray.push({ ...row, - components + components, }); - for (const component of row.components.filter(c => !filterArray.includes(c.type))) { + for (const component of row.components.filter( + c => !filterArray.includes(c.type) + )) { const spocsConfig = component.spocs; if (spocsConfig || component.feed) { // TODO make sure this still works for different loading cases. - if (component.feed && !feeds.data[component.feed.url] || spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded) { + if ( + (component.feed && !feeds.data[component.feed.url]) || + (spocsConfig && + spocsConfig.positions && + spocsConfig.positions.length && + !spocs.loaded) + ) { components.push(placeholderComponent(component)); return renderedLayoutArray; } @@ -8382,11 +8504,12 @@ const selectLayoutRender = ({ } return renderedLayoutArray; }; + const layoutRender = renderLayout(); - return { - layoutRender - }; + + return { layoutRender }; }; + ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx /* 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, @@ -8528,19 +8651,21 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent { privacyNoticeURL: component.properties.privacyNoticeURL }); case "CollectionCardGrid": - const { - DiscoveryStream - } = this.props; - return /*#__PURE__*/external_React_default().createElement(CollectionCardGrid, { - data: component.data, - feed: component.feed, - spocs: DiscoveryStream.spocs, - placement: component.placement, - type: component.type, - items: component.properties.items, - dismissible: this.props.DiscoveryStream.isCollectionDismissible, - dispatch: this.props.dispatch - }); + { + const { + DiscoveryStream + } = this.props; + return /*#__PURE__*/external_React_default().createElement(CollectionCardGrid, { + data: component.data, + feed: component.feed, + spocs: DiscoveryStream.spocs, + placement: component.placement, + type: component.type, + items: component.properties.items, + dismissible: this.props.DiscoveryStream.isCollectionDismissible, + dispatch: this.props.dispatch + }); + } case "CardGrid": return /*#__PURE__*/external_React_default().createElement(CardGrid, { title: component.header && component.header.title, @@ -8561,7 +8686,8 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent { spocMessageVariant: component.properties.spocMessageVariant, editorsPicksHeader: component.properties.editorsPicksHeader, recentSavesEnabled: this.props.DiscoveryStream.recentSavesEnabled, - hideDescriptions: this.props.DiscoveryStream.hideDescriptions + hideDescriptions: this.props.DiscoveryStream.hideDescriptions, + firstVisibleTimestamp: this.props.firstVisibleTimestamp }); case "HorizontalRule": return /*#__PURE__*/external_React_default().createElement(HorizontalRule, null); @@ -8718,20 +8844,87 @@ const DiscoveryStreamBase = (0,external_ReactRedux_namespaceObject.connect)(stat DiscoveryStream: state.DiscoveryStream, Prefs: state.Prefs, Sections: state.Sections, - document: __webpack_require__.g.document, + document: globalThis.document, App: state.App }))(_DiscoveryStreamBase); -;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/BackgroundsSection/BackgroundsSection.jsx +;// CONCATENATED MODULE: ./content-src/components/WallpapersSection/WallpapersSection.jsx /* 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/. */ -class BackgroundsSection extends (external_React_default()).PureComponent { + +class _WallpapersSection extends (external_React_default()).PureComponent { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + this.handleReset = this.handleReset.bind(this); + this.prefersHighContrastQuery = null; + this.prefersDarkQuery = null; + } + componentDidMount() { + this.prefersDarkQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"); + } + handleChange(event) { + const { + id + } = event.target; + const prefs = this.props.Prefs.values; + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, id); + // bug 1892095 + if (prefs["newtabWallpapers.wallpaper-dark"] === "" && colorMode === "light") { + this.props.setPref("newtabWallpapers.wallpaper-dark", id.replace("light", "dark")); + } + if (prefs["newtabWallpapers.wallpaper-light"] === "" && colorMode === "dark") { + this.props.setPref(`newtabWallpapers.wallpaper-light`, id.replace("dark", "light")); + } + } + handleReset() { + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.props.setPref(`newtabWallpapers.wallpaper-${colorMode}`, ""); + } render() { - return /*#__PURE__*/external_React_default().createElement("div", null); + const { + wallpaperList + } = this.props.Wallpapers; + const { + activeWallpaper + } = this.props; + return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("fieldset", { + className: "wallpaper-list" + }, wallpaperList.map(({ + title, + theme, + fluent_id + }) => { + return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("input", { + onChange: this.handleChange, + type: "radio", + name: `wallpaper-${title}`, + id: title, + value: title, + checked: title === activeWallpaper, + "aria-checked": title === activeWallpaper, + className: `wallpaper-input theme-${theme} ${title}` + }), /*#__PURE__*/external_React_default().createElement("label", { + htmlFor: title, + className: "sr-only", + "data-l10n-id": fluent_id + }, fluent_id)); + })), /*#__PURE__*/external_React_default().createElement("button", { + className: "wallpapers-reset", + onClick: this.handleReset, + "data-l10n-id": "newtab-wallpaper-reset" + })); } } +const WallpapersSection = (0,external_ReactRedux_namespaceObject.connect)(state => { + return { + Wallpapers: state.Wallpapers, + Prefs: state.Prefs + }; +})(_WallpapersSection); ;// CONCATENATED MODULE: ./content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx /* 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, @@ -8740,6 +8933,7 @@ class BackgroundsSection extends (external_React_default()).PureComponent { + class ContentSection extends (external_React_default()).PureComponent { constructor(props) { super(props); @@ -8818,7 +9012,10 @@ class ContentSection extends (external_React_default()).PureComponent { mayHaveSponsoredStories, mayHaveRecentSaves, openPreferences, - spocMessageVariant + spocMessageVariant, + wallpapersEnabled, + activeWallpaper, + setPref } = this.props; const { topSitesEnabled, @@ -8831,7 +9028,14 @@ class ContentSection extends (external_React_default()).PureComponent { } = enabledSections; return /*#__PURE__*/external_React_default().createElement("div", { className: "home-section" - }, /*#__PURE__*/external_React_default().createElement("div", { + }, wallpapersEnabled && /*#__PURE__*/external_React_default().createElement("div", { + className: "wallpapers-section" + }, /*#__PURE__*/external_React_default().createElement("h2", { + "data-l10n-id": "newtab-wallpaper-title" + }), /*#__PURE__*/external_React_default().createElement(WallpapersSection, { + setPref: setPref, + activeWallpaper: activeWallpaper + })), /*#__PURE__*/external_React_default().createElement("div", { id: "shortcuts-section", className: "section" }, /*#__PURE__*/external_React_default().createElement("moz-toggle", { @@ -8979,7 +9183,6 @@ class ContentSection extends (external_React_default()).PureComponent { - class _CustomizeMenu extends (external_React_default()).PureComponent { constructor(props) { super(props); @@ -9023,10 +9226,12 @@ class _CustomizeMenu extends (external_React_default()).PureComponent { className: "close-button", "data-l10n-id": "newtab-custom-close-button", ref: c => this.closeButton = c - }), /*#__PURE__*/external_React_default().createElement(BackgroundsSection, null), /*#__PURE__*/external_React_default().createElement(ContentSection, { + }), /*#__PURE__*/external_React_default().createElement(ContentSection, { openPreferences: this.props.openPreferences, setPref: this.props.setPref, enabledSections: this.props.enabledSections, + wallpapersEnabled: this.props.wallpapersEnabled, + activeWallpaper: this.props.activeWallpaper, pocketRegion: this.props.pocketRegion, mayHaveSponsoredTopSites: this.props.mayHaveSponsoredTopSites, mayHaveSponsoredStories: this.props.mayHaveSponsoredStories, @@ -9039,44 +9244,46 @@ class _CustomizeMenu extends (external_React_default()).PureComponent { const CustomizeMenu = (0,external_ReactRedux_namespaceObject.connect)(state => ({ DiscoveryStream: state.DiscoveryStream }))(_CustomizeMenu); -;// CONCATENATED MODULE: ./content-src/lib/constants.js +;// CONCATENATED MODULE: ./content-src/lib/constants.mjs /* 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/. */ -const IS_NEWTAB = __webpack_require__.g.document && __webpack_require__.g.document.documentURI === "about:newtab"; +const IS_NEWTAB = + globalThis.document && globalThis.document.documentURI === "about:newtab"; const NEWTAB_DARK_THEME = { ntp_background: { r: 42, g: 42, b: 46, - a: 1 + a: 1, }, ntp_card_background: { r: 66, g: 65, b: 77, - a: 1 + a: 1, }, ntp_text: { r: 249, g: 249, b: 250, - a: 1 + a: 1, }, sidebar: { r: 56, g: 56, b: 61, - a: 1 + a: 1, }, sidebar_text: { r: 249, g: 249, b: 250, - a: 1 - } + a: 1, + }, }; + ;// CONCATENATED MODULE: ./content-src/components/Search/Search.jsx /* 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, @@ -9258,6 +9465,8 @@ function Base_extends() { Base_extends = Object.assign ? Object.assign.bind() : +const Base_VISIBLE = "visible"; +const Base_VISIBILITY_CHANGE_EVENT = "visibilitychange"; const PrefsButton = ({ onClick, icon @@ -9306,7 +9515,7 @@ class _Base extends (external_React_default()).PureComponent { // If we skipped the about:welcome overlay and removed the CSS classes // we don't want to add them back to the Activity Stream view document.body.classList.contains("inline-onboarding") ? "inline-onboarding" : ""].filter(v => v).join(" "); - __webpack_require__.g.document.body.className = bodyClassName; + globalThis.document.body.className = bodyClassName; } render() { const { @@ -9337,17 +9546,55 @@ class BaseContent extends (external_React_default()).PureComponent { this.handleOnKeyDown = this.handleOnKeyDown.bind(this); this.onWindowScroll = debounce(this.onWindowScroll.bind(this), 5); this.setPref = this.setPref.bind(this); + this.updateWallpaper = this.updateWallpaper.bind(this); + this.prefersDarkQuery = null; + this.handleColorModeChange = this.handleColorModeChange.bind(this); this.state = { - fixedSearch: false + fixedSearch: false, + firstVisibleTimestamp: null, + colorMode: "" }; } + setFirstVisibleTimestamp() { + if (!this.state.firstVisibleTimestamp) { + this.setState({ + firstVisibleTimestamp: Date.now() + }); + } + } componentDidMount() { __webpack_require__.g.addEventListener("scroll", this.onWindowScroll); __webpack_require__.g.addEventListener("keydown", this.handleOnKeyDown); + if (this.props.document.visibilityState === Base_VISIBLE) { + this.setFirstVisibleTimestamp(); + } else { + this._onVisibilityChange = () => { + if (this.props.document.visibilityState === Base_VISIBLE) { + this.setFirstVisibleTimestamp(); + this.props.document.removeEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + this._onVisibilityChange = null; + } + }; + this.props.document.addEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + } + // track change event to dark/light mode + this.prefersDarkQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"); + this.prefersDarkQuery.addEventListener("change", this.handleColorModeChange); + this.handleColorModeChange(); + } + handleColorModeChange() { + const colorMode = this.prefersDarkQuery?.matches ? "dark" : "light"; + this.setState({ + colorMode + }); } componentWillUnmount() { + this.prefersDarkQuery?.removeEventListener("change", this.handleColorModeChange); __webpack_require__.g.removeEventListener("scroll", this.onWindowScroll); __webpack_require__.g.removeEventListener("keydown", this.handleOnKeyDown); + if (this._onVisibilityChange) { + this.props.document.removeEventListener(Base_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + } } onWindowScroll() { const prefs = this.props.Prefs.values; @@ -9396,6 +9643,53 @@ class BaseContent extends (external_React_default()).PureComponent { setPref(pref, value) { this.props.dispatch(actionCreators.SetPref(pref, value)); } + renderWallpaperAttribution() { + const { + wallpaperList + } = this.props.Wallpapers; + const activeWallpaper = this.props.Prefs.values[`newtabWallpapers.wallpaper-${this.state.colorMode}`]; + const selected = wallpaperList.find(wp => wp.title === activeWallpaper); + // make sure a wallpaper is selected and that the attribution also exists + if (!selected?.attribution) { + return null; + } + const { + name, + webpage + } = selected.attribution; + if (activeWallpaper && wallpaperList && name.url) { + return /*#__PURE__*/external_React_default().createElement("p", { + className: `wallpaper-attribution`, + key: name, + "data-l10n-id": "newtab-wallpaper-attribution", + "data-l10n-args": JSON.stringify({ + author_string: name.string, + author_url: name.url, + webpage_string: webpage.string, + webpage_url: webpage.url + }) + }, /*#__PURE__*/external_React_default().createElement("a", { + "data-l10n-name": "name-link", + href: name.url + }, name.string), /*#__PURE__*/external_React_default().createElement("a", { + "data-l10n-name": "webpage-link", + href: webpage.url + }, webpage.string)); + } + return null; + } + async updateWallpaper() { + const prefs = this.props.Prefs.values; + const { + wallpaperList + } = this.props.Wallpapers; + if (wallpaperList) { + const lightWallpaper = wallpaperList.find(wp => wp.title === prefs["newtabWallpapers.wallpaper-light"]) || ""; + const darkWallpaper = wallpaperList.find(wp => wp.title === prefs["newtabWallpapers.wallpaper-dark"]) || ""; + __webpack_require__.g.document?.body.style.setProperty(`--newtab-wallpaper-light`, `url(${lightWallpaper?.wallpaperUrl || ""})`); + __webpack_require__.g.document?.body.style.setProperty(`--newtab-wallpaper-dark`, `url(${darkWallpaper?.wallpaperUrl || ""})`); + } + } render() { const { props @@ -9408,6 +9702,8 @@ class BaseContent extends (external_React_default()).PureComponent { customizeMenuVisible } = App; const prefs = props.Prefs.values; + const activeWallpaper = prefs[`newtabWallpapers.wallpaper-${this.state.colorMode}`]; + const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; const { pocketConfig } = prefs; @@ -9435,12 +9731,17 @@ class BaseContent extends (external_React_default()).PureComponent { mayHaveSponsoredTopSites } = prefs; const outerClassName = ["outer-wrapper", isDiscoveryStream && pocketEnabled && "ds-outer-wrapper-search-alignment", isDiscoveryStream && "ds-outer-wrapper-breakpoint-override", prefs.showSearch && this.state.fixedSearch && !noSectionsEnabled && "fixed-search", prefs.showSearch && noSectionsEnabled && "only-search", prefs["logowordmark.alwaysVisible"] && "visible-logo"].filter(v => v).join(" "); + if (wallpapersEnabled) { + this.updateWallpaper(); + } return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement(CustomizeMenu, { onClose: this.closeCustomizationMenu, onOpen: this.openCustomizationMenu, openPreferences: this.openPreferences, setPref: this.setPref, enabledSections: enabledSections, + wallpapersEnabled: wallpapersEnabled, + activeWallpaper: activeWallpaper, pocketRegion: pocketRegion, mayHaveSponsoredTopSites: mayHaveSponsoredTopSites, mayHaveSponsoredStories: mayHaveSponsoredStories, @@ -9460,31 +9761,38 @@ class BaseContent extends (external_React_default()).PureComponent { className: "borderless-error" }, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamBase, { locale: props.App.locale, - mayHaveSponsoredStories: mayHaveSponsoredStories - })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null)))); + mayHaveSponsoredStories: mayHaveSponsoredStories, + firstVisibleTimestamp: this.state.firstVisibleTimestamp + })) : /*#__PURE__*/external_React_default().createElement(Sections_Sections, null)), /*#__PURE__*/external_React_default().createElement(ConfirmDialog, null), wallpapersEnabled && this.renderWallpaperAttribution()))); } } +BaseContent.defaultProps = { + document: __webpack_require__.g.document +}; const Base = (0,external_ReactRedux_namespaceObject.connect)(state => ({ App: state.App, Prefs: state.Prefs, Sections: state.Sections, DiscoveryStream: state.DiscoveryStream, - Search: state.Search + Search: state.Search, + Wallpapers: state.Wallpapers }))(_Base); -;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.js +;// CONCATENATED MODULE: ./content-src/lib/detect-user-session-start.mjs /* 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/. */ + const detect_user_session_start_VISIBLE = "visible"; const detect_user_session_start_VISIBILITY_CHANGE_EVENT = "visibilitychange"; + class DetectUserSessionStart { constructor(store, options = {}) { this._store = store; // Overrides for testing - this.document = options.document || __webpack_require__.g.document; + this.document = options.document || globalThis.document; this._perfService = options.perfService || perfService; this._onVisibilityChange = this._onVisibilityChange.bind(this); } @@ -9502,7 +9810,10 @@ class DetectUserSessionStart { this._sendEvent(); } else { // If the document is not visible, listen for when it does become visible. - this.document.addEventListener(detect_user_session_start_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + this.document.addEventListener( + detect_user_session_start_VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); } } @@ -9513,14 +9824,19 @@ class DetectUserSessionStart { */ _sendEvent() { this._perfService.mark("visibility_event_rcvd_ts"); + try { - let visibility_event_rcvd_ts = this._perfService.getMostRecentAbsMarkStartByName("visibility_event_rcvd_ts"); - this._store.dispatch(actionCreators.AlsoToMain({ - type: actionTypes.SAVE_SESSION_PERF_DATA, - data: { - visibility_event_rcvd_ts - } - })); + let visibility_event_rcvd_ts = + this._perfService.getMostRecentAbsMarkStartByName( + "visibility_event_rcvd_ts" + ); + + this._store.dispatch( + actionCreators.AlsoToMain({ + type: actionTypes.SAVE_SESSION_PERF_DATA, + data: { visibility_event_rcvd_ts }, + }) + ); } catch (ex) { // If this failed, it's likely because the `privacy.resistFingerprinting` // pref is true. We should at least not blow up. @@ -9534,13 +9850,17 @@ class DetectUserSessionStart { _onVisibilityChange() { if (this.document.visibilityState === detect_user_session_start_VISIBLE) { this._sendEvent(); - this.document.removeEventListener(detect_user_session_start_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange); + this.document.removeEventListener( + detect_user_session_start_VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); } } } + ;// CONCATENATED MODULE: external "Redux" const external_Redux_namespaceObject = Redux; -;// CONCATENATED MODULE: ./content-src/lib/init-store.js +;// CONCATENATED MODULE: ./content-src/lib/init-store.mjs /* 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/. */ @@ -9548,6 +9868,10 @@ const external_Redux_namespaceObject = Redux; /* eslint-env mozilla/remote-page */ +// We disable import checking here as redux is installed via the npm packages +// at the newtab level, rather than in the top-level package.json. +// eslint-disable-next-line import/no-unresolved + const MERGE_STORE_ACTION = "NEW_TAB_INITIAL_STATE"; const OUTGOING_MESSAGE_NAME = "ActivityStream:ContentToMain"; @@ -9572,11 +9896,9 @@ const INCOMING_MESSAGE_NAME = "ActivityStream:MainToContent"; function mergeStateReducer(mainReducer) { return (prevState, action) => { if (action.type === MERGE_STORE_ACTION) { - return { - ...prevState, - ...action.data - }; + return { ...prevState, ...action.data }; } + return mainReducer(prevState, action); }; } @@ -9593,9 +9915,8 @@ const messageMiddleware = () => next => action => { next(action); } }; -const rehydrationMiddleware = ({ - getState -}) => { + +const rehydrationMiddleware = ({ getState }) => { // NB: The parameter here is MiddlewareAPI which looks like a Store and shares // the same getState, so attached properties are accessible from the store. getState.didRehydrate = false; @@ -9604,17 +9925,24 @@ const rehydrationMiddleware = ({ if (getState.didRehydrate || window.__FROM_STARTUP_CACHE__) { // Startup messages can be safely ignored by the about:home document // stored in the startup cache. - if (window.__FROM_STARTUP_CACHE__ && action.meta && action.meta.isStartup) { + if ( + window.__FROM_STARTUP_CACHE__ && + action.meta && + action.meta.isStartup + ) { return null; } return next(action); } + const isMergeStoreAction = action.type === MERGE_STORE_ACTION; const isRehydrationRequest = action.type === actionTypes.NEW_TAB_STATE_REQUEST; + if (isRehydrationRequest) { getState.didRequestInitialState = true; return next(action); } + if (isMergeStoreAction) { getState.didRehydrate = true; return next(action); @@ -9622,16 +9950,20 @@ const rehydrationMiddleware = ({ // If init happened after our request was made, we need to re-request if (getState.didRequestInitialState && action.type === actionTypes.INIT) { - return next(actionCreators.AlsoToMain({ - type: actionTypes.NEW_TAB_STATE_REQUEST - })); + return next(actionCreators.AlsoToMain({ type: actionTypes.NEW_TAB_STATE_REQUEST })); } - if (actionUtils.isBroadcastToContent(action) || actionUtils.isSendToOneContent(action) || actionUtils.isSendToPreloaded(action)) { + + if ( + actionUtils.isBroadcastToContent(action) || + actionUtils.isSendToOneContent(action) || + actionUtils.isSendToPreloaded(action) + ) { // Note that actions received before didRehydrate will not be dispatched // because this could negatively affect preloading and the the state // will be replaced by rehydration anyway. return null; } + return next(action); }; }; @@ -9644,19 +9976,31 @@ const rehydrationMiddleware = ({ * @return {object} A redux store */ function initStore(reducers, initialState) { - const store = (0,external_Redux_namespaceObject.createStore)(mergeStateReducer((0,external_Redux_namespaceObject.combineReducers)(reducers)), initialState, __webpack_require__.g.RPMAddMessageListener && (0,external_Redux_namespaceObject.applyMiddleware)(rehydrationMiddleware, messageMiddleware)); - if (__webpack_require__.g.RPMAddMessageListener) { - __webpack_require__.g.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { + const store = (0,external_Redux_namespaceObject.createStore)( + mergeStateReducer((0,external_Redux_namespaceObject.combineReducers)(reducers)), + initialState, + globalThis.RPMAddMessageListener && + (0,external_Redux_namespaceObject.applyMiddleware)(rehydrationMiddleware, messageMiddleware) + ); + + if (globalThis.RPMAddMessageListener) { + globalThis.RPMAddMessageListener(INCOMING_MESSAGE_NAME, msg => { try { store.dispatch(msg.data); } catch (ex) { console.error("Content msg:", msg, "Dispatch error: ", ex); - dump(`Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ex.stack}`); + dump( + `Content msg: ${JSON.stringify(msg)}\nDispatch error: ${ex}\n${ + ex.stack + }` + ); } }); } + return store; } + ;// CONCATENATED MODULE: external "ReactDOM" const external_ReactDOM_namespaceObject = ReactDOM; var external_ReactDOM_default = /*#__PURE__*/__webpack_require__.n(external_ReactDOM_namespaceObject); diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif Binary files differnew file mode 100644 index 0000000000..5b77286079 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-color.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-color.avif Binary files differnew file mode 100644 index 0000000000..a4fc8e2341 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-color.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif Binary files differnew file mode 100644 index 0000000000..ed22325f00 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif Binary files differnew file mode 100644 index 0000000000..a704809a12 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif Binary files differnew file mode 100644 index 0000000000..decfff669b --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif Binary files differnew file mode 100644 index 0000000000..51eea392ca --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-beach.avif b/browser/components/newtab/data/content/assets/wallpapers/light-beach.avif Binary files differnew file mode 100644 index 0000000000..b5f7b2ae67 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-beach.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-color.avif b/browser/components/newtab/data/content/assets/wallpapers/light-color.avif Binary files differnew file mode 100644 index 0000000000..3366b7aec6 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-color.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif b/browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif Binary files differnew file mode 100644 index 0000000000..1776091825 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif b/browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif Binary files differnew file mode 100644 index 0000000000..5983c942fc --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-panda.avif b/browser/components/newtab/data/content/assets/wallpapers/light-panda.avif Binary files differnew file mode 100644 index 0000000000..d20f405e45 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-panda.avif diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-sky.avif b/browser/components/newtab/data/content/assets/wallpapers/light-sky.avif Binary files differnew file mode 100644 index 0000000000..f152f00e06 --- /dev/null +++ b/browser/components/newtab/data/content/assets/wallpapers/light-sky.avif diff --git a/browser/components/newtab/karma.mc.config.js b/browser/components/newtab/karma.mc.config.js index fa3ac14587..886b19df7b 100644 --- a/browser/components/newtab/karma.mc.config.js +++ b/browser/components/newtab/karma.mc.config.js @@ -158,6 +158,15 @@ module.exports = function (config) { functions: 0, branches: 0, }, + /** + * WallpaperFeed.sys.mjs is tested via an xpcshell test + */ + "lib/WallpaperFeed.sys.mjs": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, "content-src/components/DiscoveryStreamComponents/**/*.jsx": { statements: 90.48, lines: 90.48, @@ -170,6 +179,15 @@ module.exports = function (config) { functions: 60, branches: 50, }, + /** + * WallpaperSection.jsx is tested via an xpcshell test + */ + "content-src/components/WallpapersSection/*.jsx": { + statements: 0, + lines: 0, + functions: 0, + branches: 0, + }, "content-src/components/DiscoveryStreamAdmin/*.jsx": { statements: 0, lines: 0, @@ -211,7 +229,7 @@ module.exports = function (config) { devtool: "inline-source-map", // This resolve config allows us to import with paths relative to the root directory, e.g. "lib/ActivityStream.sys.mjs" resolve: { - extensions: [".js", ".jsx"], + extensions: [".js", ".jsx", ".mjs"], modules: [PATHS.moduleResolveDirectory, "node_modules"], alias: { asrouter: path.join(__dirname, "../asrouter"), @@ -260,7 +278,7 @@ module.exports = function (config) { }, { enforce: "post", - test: /\.js[mx]?$/, + test: /\.js[x]?$/, loader: "@jsdevtools/coverage-istanbul-loader", options: { esModules: true }, include: [ diff --git a/browser/components/newtab/lib/AboutPreferences.sys.mjs b/browser/components/newtab/lib/AboutPreferences.sys.mjs index 33f7ecdaeb..08e0ca422a 100644 --- a/browser/components/newtab/lib/AboutPreferences.sys.mjs +++ b/browser/components/newtab/lib/AboutPreferences.sys.mjs @@ -5,7 +5,7 @@ import { actionTypes as at, actionCreators as ac, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const HTML_NS = "http://www.w3.org/1999/xhtml"; export const PREFERENCES_LOADED_EVENT = "home-pane-loaded"; diff --git a/browser/components/newtab/lib/ActivityStream.sys.mjs b/browser/components/newtab/lib/ActivityStream.sys.mjs index f46e8aadf0..fa2d011f11 100644 --- a/browser/components/newtab/lib/ActivityStream.sys.mjs +++ b/browser/components/newtab/lib/ActivityStream.sys.mjs @@ -36,6 +36,7 @@ ChromeUtils.defineESModuleGetters(lazy, { TelemetryFeed: "resource://activity-stream/lib/TelemetryFeed.sys.mjs", TopSitesFeed: "resource://activity-stream/lib/TopSitesFeed.sys.mjs", TopStoriesFeed: "resource://activity-stream/lib/TopStoriesFeed.sys.mjs", + WallpaperFeed: "resource://activity-stream/lib/WallpaperFeed.sys.mjs", }); // NB: Eagerly load modules that will be loaded/constructed/initialized in the @@ -43,7 +44,7 @@ ChromeUtils.defineESModuleGetters(lazy, { import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const REGION_BASIC_CONFIG = "browser.newtabpage.activity-stream.discoverystream.region-basic-config"; @@ -233,6 +234,27 @@ export const PREFS_CONFIG = new Map([ }, ], [ + "newtabWallpapers.enabled", + { + title: "Boolean flag to turn wallpaper functionality on and off", + value: true, + }, + ], + [ + "newtabWallpapers.wallpaper-light", + { + title: "Currently set light wallpaper", + value: "", + }, + ], + [ + "newtabWallpapers.wallpaper-dark", + { + title: "Currently set dark wallpaper", + value: "", + }, + ], + [ "improvesearch.noDefaultSearchTile", { title: "Remove tiles that are the same as the default search", @@ -524,6 +546,12 @@ const FEEDS_DATA = [ title: "Handles new pocket ui for the new tab page", value: true, }, + { + name: "wallpaperfeed", + factory: () => new lazy.WallpaperFeed(), + title: "Handles fetching and managing wallpaper data from RemoteSettings", + value: true, + }, ]; const FEEDS_CONFIG = new Map(); diff --git a/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs b/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs index 5392a421ca..3cb81b4793 100644 --- a/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs +++ b/browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs @@ -13,7 +13,7 @@ import { actionCreators as ac, actionTypes as at, actionUtils as au, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const ABOUT_NEW_TAB_URL = "about:newtab"; diff --git a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs index ee08462503..bff9f1e04e 100644 --- a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs +++ b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs @@ -26,7 +26,7 @@ const { setTimeout, clearTimeout } = ChromeUtils.importESModule( import { actionTypes as at, actionCreators as ac, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const CACHE_KEY = "discovery_stream"; const STARTUP_CACHE_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 1 week @@ -565,8 +565,8 @@ export class DiscoveryStreamFeed { generateFeedUrl(isBff) { if (isBff) { - return `https://${lazy.NimbusFeatures.saveToPocket.getVariable( - "bffApi" + return `https://${Services.prefs.getStringPref( + "extensions.pocket.bffApi" )}/desktop/v1/recommendations?locale=$locale®ion=$region&count=30`; } return FEED_URL; @@ -986,8 +986,9 @@ export class DiscoveryStreamFeed { }); if (spocsResponse) { + const fetchTimestamp = Date.now(); spocsState = { - lastUpdated: Date.now(), + lastUpdated: fetchTimestamp, spocs: { ...spocsResponse, }, @@ -1050,8 +1051,13 @@ export class DiscoveryStreamFeed { const { data: blockedResults } = this.filterBlocked(capResult); + const { data: spocsWithFetchTimestamp } = this.addFetchTimestamp( + blockedResults, + fetchTimestamp + ); + const { data: scoredResults, personalized } = - await this.scoreItems(blockedResults, "spocs"); + await this.scoreItems(spocsWithFetchTimestamp, "spocs"); spocsState.spocs = { ...spocsState.spocs, @@ -1209,6 +1215,22 @@ export class DiscoveryStreamFeed { return { data }; } + // Add the fetch timestamp property to each spoc returned to communicate how + // old the spoc is in telemetry when it is used by the client + addFetchTimestamp(spocs, fetchTimestamp) { + if (spocs && spocs.length) { + return { + data: spocs.map(s => { + return { + ...s, + fetchTimestamp, + }; + }), + }; + } + return { data: spocs }; + } + // For backwards compatibility, older spoc endpoint don't have flight_id, // but instead had campaign_id we can use // @@ -1334,8 +1356,8 @@ export class DiscoveryStreamFeed { let options = {}; if (this.isBff) { const headers = new Headers(); - const oAuthConsumerKey = lazy.NimbusFeatures.saveToPocket.getVariable( - "oAuthConsumerKeyBff" + const oAuthConsumerKey = Services.prefs.getStringPref( + "extensions.pocket.oAuthConsumerKeyBff" ); headers.append("consumer_key", oAuthConsumerKey); options = { @@ -1768,7 +1790,7 @@ export class DiscoveryStreamFeed { break; // Check if spocs was disabled. Remove them if they were. case PREF_SHOW_SPONSORED: - case PREF_SHOW_SPONSORED_TOPSITES: + case PREF_SHOW_SPONSORED_TOPSITES: { const dispatch = update => this.store.dispatch(ac.BroadcastToContent(update)); // We refresh placements data because one of the spocs were turned off. @@ -1794,6 +1816,7 @@ export class DiscoveryStreamFeed { await this.cache.set("spocs", {}); await this.loadSpocs(dispatch); break; + } } } diff --git a/browser/components/newtab/lib/DownloadsManager.sys.mjs b/browser/components/newtab/lib/DownloadsManager.sys.mjs index a9a57222ee..f6e99e462a 100644 --- a/browser/components/newtab/lib/DownloadsManager.sys.mjs +++ b/browser/components/newtab/lib/DownloadsManager.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; const lazy = {}; diff --git a/browser/components/newtab/lib/FaviconFeed.sys.mjs b/browser/components/newtab/lib/FaviconFeed.sys.mjs index a76566d3e8..18c2231f58 100644 --- a/browser/components/newtab/lib/FaviconFeed.sys.mjs +++ b/browser/components/newtab/lib/FaviconFeed.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; import { getDomain } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs"; // We use importESModule here instead of static import so that diff --git a/browser/components/newtab/lib/HighlightsFeed.sys.mjs b/browser/components/newtab/lib/HighlightsFeed.sys.mjs index c603b886da..00eb109896 100644 --- a/browser/components/newtab/lib/HighlightsFeed.sys.mjs +++ b/browser/components/newtab/lib/HighlightsFeed.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; import { diff --git a/browser/components/newtab/lib/NewTabInit.sys.mjs b/browser/components/newtab/lib/NewTabInit.sys.mjs index db30e009ec..768cc29ea4 100644 --- a/browser/components/newtab/lib/NewTabInit.sys.mjs +++ b/browser/components/newtab/lib/NewTabInit.sys.mjs @@ -5,7 +5,7 @@ import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; /** * NewTabInit - A placeholder for now. This will send a copy of the state to all diff --git a/browser/components/newtab/lib/PlacesFeed.sys.mjs b/browser/components/newtab/lib/PlacesFeed.sys.mjs index 70011412f8..85679153bd 100644 --- a/browser/components/newtab/lib/PlacesFeed.sys.mjs +++ b/browser/components/newtab/lib/PlacesFeed.sys.mjs @@ -6,7 +6,7 @@ import { actionCreators as ac, actionTypes as at, actionUtils as au, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; diff --git a/browser/components/newtab/lib/PrefsFeed.sys.mjs b/browser/components/newtab/lib/PrefsFeed.sys.mjs index bb2502ac55..4cb41c0421 100644 --- a/browser/components/newtab/lib/PrefsFeed.sys.mjs +++ b/browser/components/newtab/lib/PrefsFeed.sys.mjs @@ -5,7 +5,7 @@ import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; // We use importESModule here instead of static import so that diff --git a/browser/components/newtab/lib/RecommendationProvider.sys.mjs b/browser/components/newtab/lib/RecommendationProvider.sys.mjs index 875c90492b..9fd6b71656 100644 --- a/browser/components/newtab/lib/RecommendationProvider.sys.mjs +++ b/browser/components/newtab/lib/RecommendationProvider.sys.mjs @@ -12,7 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, { import { actionTypes as at, actionCreators as ac, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; const CACHE_KEY = "personalization"; const PREF_PERSONALIZATION_MODEL_KEYS = diff --git a/browser/components/newtab/lib/SectionsManager.sys.mjs b/browser/components/newtab/lib/SectionsManager.sys.mjs index 069ddbb224..a1634e0d47 100644 --- a/browser/components/newtab/lib/SectionsManager.sys.mjs +++ b/browser/components/newtab/lib/SectionsManager.sys.mjs @@ -15,7 +15,7 @@ const { EventEmitter } = ChromeUtils.importESModule( import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { getDefaultOptions } from "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs"; const lazy = {}; @@ -389,7 +389,7 @@ export const SectionsManager = { /** * Sets each card in highlights' context menu options based on the card's type. - * (See types.js for a list of types) + * (See types.mjs for a list of types) * * @param rows section rows containing a type for each card */ diff --git a/browser/components/newtab/lib/SystemTickFeed.sys.mjs b/browser/components/newtab/lib/SystemTickFeed.sys.mjs index d87860fab2..fdbbda3ddd 100644 --- a/browser/components/newtab/lib/SystemTickFeed.sys.mjs +++ b/browser/components/newtab/lib/SystemTickFeed.sys.mjs @@ -2,7 +2,7 @@ * 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 { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs"; +import { actionTypes as at } from "resource://activity-stream/common/Actions.mjs"; const lazy = {}; diff --git a/browser/components/newtab/lib/TelemetryFeed.sys.mjs b/browser/components/newtab/lib/TelemetryFeed.sys.mjs index 1a9e9e3d34..6cf4dba4ab 100644 --- a/browser/components/newtab/lib/TelemetryFeed.sys.mjs +++ b/browser/components/newtab/lib/TelemetryFeed.sys.mjs @@ -18,13 +18,13 @@ const { XPCOMUtils } = ChromeUtils.importESModule( // eslint-disable-next-line mozilla/use-static-import const { MESSAGE_TYPE_HASH: msg } = ChromeUtils.importESModule( - "resource:///modules/asrouter/ActorConstants.sys.mjs" + "resource:///modules/asrouter/ActorConstants.mjs" ); import { actionTypes as at, actionUtils as au, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; import { classifySite } from "resource://activity-stream/lib/SiteClassifier.sys.mjs"; @@ -454,8 +454,7 @@ export class TelemetryFeed { event = await this.applyCFRPolicy(event); break; case "badge_user_event": - case "whats-new-panel_user_event": - event = await this.applyWhatsNewPolicy(event); + event = await this.applyToolbarBadgePolicy(event); break; case "infobar_user_event": event = await this.applyInfoBarPolicy(event); @@ -509,12 +508,12 @@ export class TelemetryFeed { * Per Bug 1482134, all the metrics for What's New panel use client_id in * all the release channels */ - async applyWhatsNewPolicy(ping) { + async applyToolbarBadgePolicy(ping) { ping.client_id = await this.telemetryClientId; ping.browser_session_id = lazy.browserSessionId; // Attach page info to `event_context` if there is a session associated with this ping delete ping.action; - return { ping, pingType: "whats-new-panel" }; + return { ping, pingType: "toolbar-badge" }; } async applyInfoBarPolicy(ping) { @@ -715,8 +714,16 @@ export class TelemetryFeed { const session = this.sessions.get(au.getPortIdOfSender(action)); switch (action.data?.event) { case "CLICK": { - const { card_type, topic, recommendation_id, tile_id, shim, feature } = - action.data.value ?? {}; + const { + card_type, + topic, + recommendation_id, + tile_id, + shim, + fetchTimestamp, + firstVisibleTimestamp, + feature, + } = action.data.value ?? {}; if ( action.data.source === "POPULAR_TOPICS" || card_type === "topics_widget" @@ -740,6 +747,14 @@ export class TelemetryFeed { }); if (shim) { Glean.pocket.shim.set(shim); + if (fetchTimestamp) { + Glean.pocket.fetchTimestamp.set(fetchTimestamp * 1000); + } + if (firstVisibleTimestamp) { + Glean.pocket.newtabCreationTimestamp.set( + firstVisibleTimestamp * 1000 + ); + } GleanPings.spoc.submit("click"); } } @@ -755,6 +770,16 @@ export class TelemetryFeed { }); if (action.data.value?.shim) { Glean.pocket.shim.set(action.data.value.shim); + if (action.data.value.fetchTimestamp) { + Glean.pocket.fetchTimestamp.set( + action.data.value.fetchTimestamp * 1000 + ); + } + if (action.data.value.newtabCreationTimestamp) { + Glean.pocket.newtabCreationTimestamp.set( + action.data.value.newtabCreationTimestamp * 1000 + ); + } GleanPings.spoc.submit("save"); } break; @@ -976,6 +1001,14 @@ export class TelemetryFeed { }); if (tile.shim) { Glean.pocket.shim.set(tile.shim); + if (tile.fetchTimestamp) { + Glean.pocket.fetchTimestamp.set(tile.fetchTimestamp * 1000); + } + if (data.firstVisibleTimestamp) { + Glean.pocket.newtabCreationTimestamp.set( + data.firstVisibleTimestamp * 1000 + ); + } GleanPings.spoc.submit("impression"); } }); diff --git a/browser/components/newtab/lib/TopSitesFeed.sys.mjs b/browser/components/newtab/lib/TopSitesFeed.sys.mjs index 796211085b..e259253402 100644 --- a/browser/components/newtab/lib/TopSitesFeed.sys.mjs +++ b/browser/components/newtab/lib/TopSitesFeed.sys.mjs @@ -5,7 +5,7 @@ import { actionCreators as ac, actionTypes as at, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { TippyTopProvider } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs"; import { insertPinned, diff --git a/browser/components/newtab/lib/TopStoriesFeed.sys.mjs b/browser/components/newtab/lib/TopStoriesFeed.sys.mjs index be030649dd..5986209a1c 100644 --- a/browser/components/newtab/lib/TopStoriesFeed.sys.mjs +++ b/browser/components/newtab/lib/TopStoriesFeed.sys.mjs @@ -5,7 +5,7 @@ import { actionTypes as at, actionCreators as ac, -} from "resource://activity-stream/common/Actions.sys.mjs"; +} from "resource://activity-stream/common/Actions.mjs"; import { Prefs } from "resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs"; import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs"; import { SectionsManager } from "resource://activity-stream/lib/SectionsManager.sys.mjs"; diff --git a/browser/components/newtab/lib/WallpaperFeed.sys.mjs b/browser/components/newtab/lib/WallpaperFeed.sys.mjs new file mode 100644 index 0000000000..cb21311ddc --- /dev/null +++ b/browser/components/newtab/lib/WallpaperFeed.sys.mjs @@ -0,0 +1,117 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + Utils: "resource://services-settings/Utils.sys.mjs", +}); + +import { + actionTypes as at, + actionCreators as ac, +} from "resource://activity-stream/common/Actions.mjs"; + +const PREF_WALLPAPERS_ENABLED = + "browser.newtabpage.activity-stream.newtabWallpapers.enabled"; + +export class WallpaperFeed { + constructor() { + this.loaded = false; + this.wallpaperClient = ""; + this.wallpaperDB = ""; + this.baseAttachmentURL = ""; + } + + /** + * This thin wrapper around global.fetch makes it easier for us to write + * automated tests that simulate responses from this fetch. + */ + fetch(...args) { + return fetch(...args); + } + + /** + * This thin wrapper around lazy.RemoteSettings makes it easier for us to write + * automated tests that simulate responses from this fetch. + */ + RemoteSettings(...args) { + return lazy.RemoteSettings(...args); + } + + async wallpaperSetup(isStartup = false) { + const wallpapersEnabled = Services.prefs.getBoolPref( + PREF_WALLPAPERS_ENABLED + ); + + if (wallpapersEnabled) { + if (!this.wallpaperClient) { + this.wallpaperClient = this.RemoteSettings("newtab-wallpapers"); + } + + await this.getBaseAttachment(); + this.wallpaperClient.on("sync", () => this.updateWallpapers()); + this.updateWallpapers(isStartup); + } + } + + async getBaseAttachment() { + if (!this.baseAttachmentURL) { + const SERVER = lazy.Utils.SERVER_URL; + const serverInfo = await ( + await this.fetch(`${SERVER}/`, { + credentials: "omit", + }) + ).json(); + const { base_url } = serverInfo.capabilities.attachments; + this.baseAttachmentURL = base_url; + } + } + + async updateWallpapers(isStartup = false) { + const records = await this.wallpaperClient.get(); + if (!records?.length) { + return; + } + + if (!this.baseAttachmentURL) { + await this.getBaseAttachment(); + } + const wallpapers = records.map(record => { + return { + ...record, + wallpaperUrl: `${this.baseAttachmentURL}${record.attachment.location}`, + }; + }); + + this.store.dispatch( + ac.BroadcastToContent({ + type: at.WALLPAPERS_SET, + data: wallpapers, + meta: { + isStartup, + }, + }) + ); + } + + async onAction(action) { + switch (action.type) { + case at.INIT: + await this.wallpaperSetup(true /* isStartup */); + break; + case at.UNINIT: + break; + case at.SYSTEM_TICK: + break; + case at.PREF_CHANGED: + if (action.data.name === "newtabWallpapers.enabled") { + await this.wallpaperSetup(false /* isStartup */); + } + break; + case at.WALLPAPERS_SET: + break; + } + } +} diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml index bd74e609ad..c59247ceef 100644 --- a/browser/components/newtab/metrics.yaml +++ b/browser/components/newtab/metrics.yaml @@ -817,6 +817,35 @@ pocket: send_in_pings: - spoc + fetch_timestamp: + type: datetime + lifetime: ping + description: | + Timestamp of when the spoc was fetched by the client + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655 + notification_emails: + - dmueller@mozilla.com + expires: never + send_in_pings: + - spoc + + newtab_creation_timestamp: + type: datetime + lifetime: ping + description: | + Timestamp of when this instance of the newtab was first visible to the user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887655 + notification_emails: + - dmueller@mozilla.com + expires: never + send_in_pings: + - spoc messaging_system: event_context_parse_error: @@ -1031,7 +1060,7 @@ messaging_system: type: string description: > Type of event the ping is capturing. - e.g. "cfr", "whats-new-panel", "onboarding" + e.g. "cfr", "onboarding" bugs: - https://bugzilla.mozilla.org/show_bug.cgi?id=1825863 data_reviews: diff --git a/browser/components/newtab/test/browser/browser_as_load_location.js b/browser/components/newtab/test/browser/browser_as_load_location.js index f11b6cf503..ce67ede0c6 100644 --- a/browser/components/newtab/test/browser/browser_as_load_location.js +++ b/browser/components/newtab/test/browser/browser_as_load_location.js @@ -8,7 +8,7 @@ */ async function checkNewtabLoads(selector, message) { // simulate a newtab open as a user would - BrowserOpenTab(); + BrowserCommands.openTab(); // wait until the browser loads let browser = gBrowser.selectedBrowser; diff --git a/browser/components/newtab/test/browser/browser_newtab_overrides.js b/browser/components/newtab/test/browser/browser_newtab_overrides.js index 1d4a0c36e3..c876a62c4e 100644 --- a/browser/components/newtab/test/browser/browser_newtab_overrides.js +++ b/browser/components/newtab/test/browser/browser_newtab_overrides.js @@ -82,7 +82,7 @@ add_task(async function override_loads_in_browser() { Assert.ok(AboutNewTab.newTabURLOverridden, "url has been overridden"); // simulate a newtab open as a user would - BrowserOpenTab(); + BrowserCommands.openTab(); let browser = gBrowser.selectedBrowser; await BrowserTestUtils.browserLoaded(browser); @@ -116,7 +116,7 @@ add_task(async function override_blank_loads_in_browser() { Assert.ok(AboutNewTab.newTabURLOverridden, "url has been overridden"); // simulate a newtab open as a user would - BrowserOpenTab(); + BrowserCommands.openTab(); let browser = gBrowser.selectedBrowser; await BrowserTestUtils.browserLoaded(browser); diff --git a/browser/components/newtab/test/schemas/pings.js b/browser/components/newtab/test/schemas/pings.js index fb52602bd4..2a1dd35ec6 100644 --- a/browser/components/newtab/test/schemas/pings.js +++ b/browser/components/newtab/test/schemas/pings.js @@ -1,7 +1,4 @@ -import { - CONTENT_MESSAGE_TYPE, - MAIN_MESSAGE_TYPE, -} from "common/Actions.sys.mjs"; +import { CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE } from "common/Actions.mjs"; import Joi from "joi-browser"; export const baseKeys = { diff --git a/browser/components/newtab/test/unit/common/Actions.test.js b/browser/components/newtab/test/unit/common/Actions.test.js index 32e417ea3f..af8d18cee8 100644 --- a/browser/components/newtab/test/unit/common/Actions.test.js +++ b/browser/components/newtab/test/unit/common/Actions.test.js @@ -8,7 +8,7 @@ import { MAIN_MESSAGE_TYPE, PRELOAD_MESSAGE_TYPE, UI_CODE, -} from "common/Actions.sys.mjs"; +} from "common/Actions.mjs"; describe("Actions", () => { it("should set globalImportContext to UI_CODE", () => { diff --git a/browser/components/newtab/test/unit/common/Reducers.test.js b/browser/components/newtab/test/unit/common/Reducers.test.js index 7343fc6224..62f6f48353 100644 --- a/browser/components/newtab/test/unit/common/Reducers.test.js +++ b/browser/components/newtab/test/unit/common/Reducers.test.js @@ -11,7 +11,7 @@ const { Search, ASRouter, } = reducers; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; describe("Reducers", () => { describe("App", () => { diff --git a/browser/components/newtab/test/unit/content-src/components/Base.test.jsx b/browser/components/newtab/test/unit/content-src/components/Base.test.jsx index c764348006..d8d300a3c9 100644 --- a/browser/components/newtab/test/unit/content-src/components/Base.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/Base.test.jsx @@ -8,7 +8,7 @@ import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundar import React from "react"; import { Search } from "content-src/components/Search/Search"; import { shallow } from "enzyme"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; describe("<Base>", () => { let DEFAULT_PROPS = { @@ -21,6 +21,11 @@ describe("<Base>", () => { adminContent: { message: {}, }, + document: { + visibilityState: "visible", + addEventListener: sinon.stub(), + removeEventListener: sinon.stub(), + }, }; it("should render Base component", () => { @@ -76,6 +81,11 @@ describe("<BaseContent>", () => { Sections: [], DiscoveryStream: { config: { enabled: false } }, dispatch: () => {}, + document: { + visibilityState: "visible", + addEventListener: sinon.stub(), + removeEventListener: sinon.stub(), + }, }; it("should render an ErrorBoundary with a Search child", () => { @@ -114,6 +124,73 @@ describe("<BaseContent>", () => { const wrapper = shallow(<BaseContent {...onlySearchProps} />); assert.lengthOf(wrapper.find(".only-search"), 1); }); + + it("should update firstVisibleTimestamp if it is visible immediately with no event listener", () => { + const props = Object.assign({}, DEFAULT_PROPS, { + document: { + visibilityState: "visible", + addEventListener: sinon.spy(), + removeEventListener: sinon.spy(), + }, + }); + + const wrapper = shallow(<BaseContent {...props} />); + assert.notCalled(props.document.addEventListener); + assert.isDefined(wrapper.state("firstVisibleTimestamp")); + }); + it("should attach an event listener for visibility change if it is not visible", () => { + const props = Object.assign({}, DEFAULT_PROPS, { + document: { + visibilityState: "hidden", + addEventListener: sinon.spy(), + removeEventListener: sinon.spy(), + }, + }); + + const wrapper = shallow(<BaseContent {...props} />); + assert.calledWith(props.document.addEventListener, "visibilitychange"); + assert.notExists(wrapper.state("firstVisibleTimestamp")); + }); + it("should remove the event listener for visibility change when unmounted", () => { + const props = Object.assign({}, DEFAULT_PROPS, { + document: { + visibilityState: "hidden", + addEventListener: sinon.spy(), + removeEventListener: sinon.spy(), + }, + }); + + const wrapper = shallow(<BaseContent {...props} />); + const [, listener] = props.document.addEventListener.firstCall.args; + + wrapper.unmount(); + assert.calledWith( + props.document.removeEventListener, + "visibilitychange", + listener + ); + }); + it("should remove the event listener for visibility change after becoming visible", () => { + const listeners = new Set(); + const props = Object.assign({}, DEFAULT_PROPS, { + document: { + visibilityState: "hidden", + addEventListener: (ev, cb) => listeners.add(cb), + removeEventListener: (ev, cb) => listeners.delete(cb), + }, + }); + + const wrapper = shallow(<BaseContent {...props} />); + assert.equal(listeners.size, 1); + assert.notExists(wrapper.state("firstVisibleTimestamp")); + + // Simulate listeners getting called + props.document.visibilityState = "visible"; + listeners.forEach(l => l()); + + assert.equal(listeners.size, 0); + assert.isDefined(wrapper.state("firstVisibleTimestamp")); + }); }); describe("<PrefsButton>", () => { diff --git a/browser/components/newtab/test/unit/content-src/components/Card.test.jsx b/browser/components/newtab/test/unit/content-src/components/Card.test.jsx index 5f07570b2e..f7f065efae 100644 --- a/browser/components/newtab/test/unit/content-src/components/Card.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/Card.test.jsx @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { _Card as Card, PlaceholderCard, diff --git a/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx b/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx index baf203947e..fcc1dd0f45 100644 --- a/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer"; import createMockRaf from "mock-raf"; import React from "react"; diff --git a/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx b/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx index a471c09e66..3befa4403f 100644 --- a/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { _ConfirmDialog as ConfirmDialog } from "content-src/components/ConfirmDialog/ConfirmDialog"; import React from "react"; import { shallow } from "enzyme"; diff --git a/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx b/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx index e1f84f7d84..0407622cf9 100644 --- a/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx @@ -1,4 +1,4 @@ -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import { ContentSection } from "content-src/components/CustomizeMenu/ContentSection/ContentSection"; import { mount } from "enzyme"; import React from "react"; diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx index 41849fba3e..7f40b66200 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DiscoveryStreamAdminInner, CollapseToggle, diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx index 418a731ba1..ffa32bfc3e 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx @@ -13,10 +13,7 @@ import { PlaceholderDSCard, } from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard"; import { TopicsWidget } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import React from "react"; import { shallow, mount } from "enzyme"; diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx index 1d572ee3ce..afb6d6dcd2 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx @@ -10,10 +10,7 @@ import { StatusMessage, SponsorLabel, } from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DSLinkMenu } from "content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu"; import React from "react"; import { INITIAL_STATE } from "common/Reducers.sys.mjs"; @@ -28,6 +25,8 @@ const DEFAULT_PROPS = { isForStartupCache: false, }, DiscoveryStream: INITIAL_STATE.DiscoveryStream, + fetchTimestamp: new Date("March 20, 2024 10:30:44").getTime(), + firstVisibleTimestamp: new Date("March 21, 2024 10:11:12").getTime(), }; describe("<DSCard>", () => { @@ -174,6 +173,8 @@ describe("<DSCard>", () => { card_type: "organic", recommendation_id: undefined, tile_id: "fooidx", + fetchTimestamp: DEFAULT_PROPS.fetchTimestamp, + firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, }, }) ); @@ -212,6 +213,8 @@ describe("<DSCard>", () => { card_type: "spoc", recommendation_id: undefined, tile_id: "fooidx", + fetchTimestamp: DEFAULT_PROPS.fetchTimestamp, + firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, }, }) ); @@ -258,6 +261,8 @@ describe("<DSCard>", () => { recommendation_id: undefined, tile_id: "fooidx", shim: "click shim", + fetchTimestamp: DEFAULT_PROPS.fetchTimestamp, + firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, }, }) ); @@ -370,7 +375,12 @@ describe("<DSCard>", () => { describe("DSCard onSaveClick", () => { it("should fire telemetry for onSaveClick", () => { - wrapper.setProps({ id: "fooidx", pos: 1, type: "foo" }); + wrapper.setProps({ + id: "fooidx", + pos: 1, + type: "foo", + fetchTimestamp: undefined, + }); wrapper.instance().onSaveClick(); assert.calledThrice(dispatch); @@ -391,6 +401,8 @@ describe("<DSCard>", () => { card_type: "organic", recommendation_id: undefined, tile_id: "fooidx", + fetchTimestamp: undefined, + firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp, }, }) ); diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx index 08ac7868ce..a18e688758 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx @@ -5,7 +5,7 @@ import { } from "content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter"; import React from "react"; import { mount } from "enzyme"; -import { cardContextTypes } from "content-src/components/Card/types.js"; +import { cardContextTypes } from "content-src/components/Card/types.mjs"; import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText.jsx"; describe("<DSContextFooter>", () => { diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx index b4b743c7ff..b5acbf3b56 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx @@ -1,6 +1,6 @@ import { DSPrivacyModal } from "content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal"; import { shallow, mount } from "enzyme"; -import { actionCreators as ac } from "common/Actions.sys.mjs"; +import { actionCreators as ac } from "common/Actions.mjs"; import React from "react"; describe("Discovery Stream <DSPrivacyModal>", () => { diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx index 4926cc6c70..c935acde1a 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx @@ -2,7 +2,7 @@ import { ImpressionStats, INTERSECTION_RATIO, } from "content-src/components/DiscoveryStreamImpressionStats/ImpressionStats"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import React from "react"; import { shallow } from "enzyme"; @@ -33,12 +33,15 @@ describe("<ImpressionStats>", () => { }; } + const TEST_FETCH_TIMESTAMP = Date.now(); + const TEST_FIRST_VISIBLE_TIMESTAMP = Date.now(); const DEFAULT_PROPS = { rows: [ - { id: 1, pos: 0 }, - { id: 2, pos: 1 }, - { id: 3, pos: 2 }, + { id: 1, pos: 0, fetchTimestamp: TEST_FETCH_TIMESTAMP }, + { id: 2, pos: 1, fetchTimestamp: TEST_FETCH_TIMESTAMP }, + { id: 3, pos: 2, fetchTimestamp: TEST_FETCH_TIMESTAMP }, ], + firstVisibleTimestamp: TEST_FIRST_VISIBLE_TIMESTAMP, source: SOURCE, IntersectionObserver: buildIntersectionObserver(FullIntersectEntries), document: { @@ -76,7 +79,7 @@ describe("<ImpressionStats>", () => { assert.notCalled(dispatch); }); - it("should noly send loaded content but not impression when the wrapped item is not visbible", () => { + it("should only send loaded content but not impression when the wrapped item is not visbible", () => { const dispatch = sinon.spy(); const props = { dispatch, @@ -128,11 +131,37 @@ describe("<ImpressionStats>", () => { [action] = dispatch.secondCall.args; assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS); assert.equal(action.data.source, SOURCE); + assert.equal( + action.data.firstVisibleTimestamp, + TEST_FIRST_VISIBLE_TIMESTAMP + ); assert.deepEqual(action.data.tiles, [ - { id: 1, pos: 0, type: "organic", recommendation_id: undefined }, - { id: 2, pos: 1, type: "organic", recommendation_id: undefined }, - { id: 3, pos: 2, type: "organic", recommendation_id: undefined }, + { + id: 1, + pos: 0, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, + { + id: 2, + pos: 1, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, + { + id: 3, + pos: 2, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, ]); + assert.equal( + action.data.firstVisibleTimestamp, + TEST_FIRST_VISIBLE_TIMESTAMP + ); }); it("should send a DISCOVERY_STREAM_SPOC_IMPRESSION when the wrapped item has a flightId", () => { const dispatch = sinon.spy(); @@ -207,10 +236,32 @@ describe("<ImpressionStats>", () => { [action] = dispatch.firstCall.args; assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS); assert.deepEqual(action.data.tiles, [ - { id: 1, pos: 0, type: "organic", recommendation_id: undefined }, - { id: 2, pos: 1, type: "organic", recommendation_id: undefined }, - { id: 3, pos: 2, type: "organic", recommendation_id: undefined }, + { + id: 1, + pos: 0, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, + { + id: 2, + pos: 1, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, + { + id: 3, + pos: 2, + type: "organic", + recommendation_id: undefined, + fetchTimestamp: TEST_FETCH_TIMESTAMP, + }, ]); + assert.equal( + action.data.firstVisibleTimestamp, + TEST_FIRST_VISIBLE_TIMESTAMP + ); }); it("should remove visibility change listener when the wrapper is removed", () => { const props = { diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx index f879600a8f..5c9dcb4c14 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx @@ -6,10 +6,7 @@ import { TopicsWidget, } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget"; import { SafeAnchor } from "content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor"; -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { mount } from "enzyme"; import React from "react"; diff --git a/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx b/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx index 9f4008369a..69d023c668 100644 --- a/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/Sections.test.jsx @@ -5,7 +5,7 @@ import { SectionIntl, _Sections as Sections, } from "content-src/components/Sections/Sections"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { mount, shallow } from "enzyme"; import { PlaceholderCard } from "content-src/components/Card/Card"; import { PocketLoggedInCta } from "content-src/components/PocketLoggedInCta/PocketLoggedInCta"; diff --git a/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx b/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx index 798bb9b8c7..9797a4863e 100644 --- a/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; import { MIN_RICH_FAVICON_SIZE } from "content-src/components/TopSites/TopSitesConstants"; import { diff --git a/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx b/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx index 3f7e725de0..b1b501ca44 100644 --- a/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx @@ -2,7 +2,7 @@ import { TopSiteImpressionWrapper, INTERSECTION_RATIO, } from "content-src/components/TopSites/TopSiteImpressionWrapper"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import React from "react"; import { shallow } from "enzyme"; diff --git a/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js b/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js index 5a7fad7cc0..3629bb7a68 100644 --- a/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js +++ b/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start"; describe("detectUserSessionStart", () => { diff --git a/browser/components/newtab/test/unit/content-src/lib/init-store.test.js b/browser/components/newtab/test/unit/content-src/lib/init-store.test.js index 0dd510ef1a..8f998b64d0 100644 --- a/browser/components/newtab/test/unit/content-src/lib/init-store.test.js +++ b/browser/components/newtab/test/unit/content-src/lib/init-store.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { addNumberReducer, GlobalOverrider } from "test/unit/utils"; import { INCOMING_MESSAGE_NAME, diff --git a/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js index 233f31b6ca..fb28c9490b 100644 --- a/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js +++ b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js @@ -1,5 +1,5 @@ import { combineReducers, createStore } from "redux"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; import { reducers } from "common/Reducers.sys.mjs"; import { selectLayoutRender } from "content-src/lib/selectLayoutRender"; diff --git a/browser/components/newtab/test/unit/lib/AboutPreferences.test.js b/browser/components/newtab/test/unit/lib/AboutPreferences.test.js index 20765608fa..a19bf698d9 100644 --- a/browser/components/newtab/test/unit/lib/AboutPreferences.test.js +++ b/browser/components/newtab/test/unit/lib/AboutPreferences.test.js @@ -3,10 +3,7 @@ import { AboutPreferences, PREFERENCES_LOADED_EVENT, } from "lib/AboutPreferences.sys.mjs"; -import { - actionTypes as at, - actionCreators as ac, -} from "common/Actions.sys.mjs"; +import { actionTypes as at, actionCreators as ac } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; describe("AboutPreferences Feed", () => { diff --git a/browser/components/newtab/test/unit/lib/ActivityStream.test.js b/browser/components/newtab/test/unit/lib/ActivityStream.test.js index b9deba1069..7921ae2c91 100644 --- a/browser/components/newtab/test/unit/lib/ActivityStream.test.js +++ b/browser/components/newtab/test/unit/lib/ActivityStream.test.js @@ -1,4 +1,4 @@ -import { CONTENT_MESSAGE_TYPE } from "common/Actions.sys.mjs"; +import { CONTENT_MESSAGE_TYPE } from "common/Actions.mjs"; import { ActivityStream, PREFS_CONFIG } from "lib/ActivityStream.sys.mjs"; import { GlobalOverrider } from "test/unit/utils"; diff --git a/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js b/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js index 4bea86331d..8df62b2903 100644 --- a/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js +++ b/browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { ActivityStreamMessageChannel, DEFAULT_OPTIONS, @@ -16,7 +13,7 @@ const OPTIONS = [ ]; // Create an object containing details about a tab as expected within -// the loaded tabs map in ActivityStreamMessageChannel.jsm. +// the loaded tabs map in ActivityStreamMessageChannel.sys.mjs. function getTabDetails(portID, url = "about:newtab", extraArgs = {}) { let actor = { portID, diff --git a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js index 92e10facb3..e10a4cbc04 100644 --- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js +++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js @@ -2,7 +2,7 @@ import { actionCreators as ac, actionTypes as at, actionUtils as au, -} from "common/Actions.sys.mjs"; +} from "common/Actions.mjs"; import { combineReducers, createStore } from "redux"; import { GlobalOverrider } from "test/unit/utils"; import { DiscoveryStreamFeed } from "lib/DiscoveryStreamFeed.sys.mjs"; @@ -849,6 +849,8 @@ describe("DiscoveryStreamFeed", () => { spocs: { items: [{ id: "data" }] }, }); sandbox.stub(feed.cache, "set").returns(Promise.resolve()); + const loadTimestamp = 100; + clock.tick(loadTimestamp); await feed.loadSpocs(feed.store.dispatch); @@ -860,15 +862,15 @@ describe("DiscoveryStreamFeed", () => { title: "", sponsor: "", sponsored_by_override: undefined, - items: [{ id: "data", score: 1 }], + items: [{ id: "data", score: 1, fetchTimestamp: loadTimestamp }], }, }, - lastUpdated: 0, + lastUpdated: loadTimestamp, }); assert.deepEqual( feed.store.getState().DiscoveryStream.spocs.data.spocs.items[0], - { id: "data", score: 1 } + { id: "data", score: 1, fetchTimestamp: loadTimestamp } ); }); it("should normalizeSpocsItems for older spoc data", async () => { @@ -882,7 +884,7 @@ describe("DiscoveryStreamFeed", () => { assert.deepEqual( feed.store.getState().DiscoveryStream.spocs.data.spocs.items[0], - { id: "data", score: 1 } + { id: "data", score: 1, fetchTimestamp: 0 } ); }); it("should dispatch DISCOVERY_STREAM_PERSONALIZATION_OVERRIDE with feature_flags", async () => { @@ -936,7 +938,7 @@ describe("DiscoveryStreamFeed", () => { context: "", sponsor: "", sponsored_by_override: undefined, - items: [{ id: "data", score: 1 }], + items: [{ id: "data", score: 1, fetchTimestamp: 0 }], }, placement2: { title: "", @@ -978,7 +980,7 @@ describe("DiscoveryStreamFeed", () => { context: "context", sponsor: "", sponsored_by_override: undefined, - items: [{ id: "data", score: 1 }], + items: [{ id: "data", score: 1, fetchTimestamp: 0 }], }, }); }); @@ -3444,16 +3446,12 @@ describe("DiscoveryStreamFeed", () => { }, }); sandbox.stub(global.Region, "home").get(() => "DE"); - globals.set("NimbusFeatures", { - saveToPocket: { - getVariable: sandbox.stub(), - }, - }); - global.NimbusFeatures.saveToPocket.getVariable - .withArgs("bffApi") + sandbox.stub(global.Services.prefs, "getStringPref"); + global.Services.prefs.getStringPref + .withArgs("extensions.pocket.bffApi") .returns("bffApi"); - global.NimbusFeatures.saveToPocket.getVariable - .withArgs("oAuthConsumerKeyBff") + global.Services.prefs.getStringPref + .withArgs("extensions.pocket.oAuthConsumerKeyBff") .returns("oAuthConsumerKeyBff"); }); it("should return true with isBff", async () => { diff --git a/browser/components/newtab/test/unit/lib/DownloadsManager.test.js b/browser/components/newtab/test/unit/lib/DownloadsManager.test.js index ac262baf90..5e2979893d 100644 --- a/browser/components/newtab/test/unit/lib/DownloadsManager.test.js +++ b/browser/components/newtab/test/unit/lib/DownloadsManager.test.js @@ -1,4 +1,4 @@ -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { DownloadsManager } from "lib/DownloadsManager.sys.mjs"; import { GlobalOverrider } from "test/unit/utils"; diff --git a/browser/components/newtab/test/unit/lib/FaviconFeed.test.js b/browser/components/newtab/test/unit/lib/FaviconFeed.test.js index e9be9b86ba..8b9cf24984 100644 --- a/browser/components/newtab/test/unit/lib/FaviconFeed.test.js +++ b/browser/components/newtab/test/unit/lib/FaviconFeed.test.js @@ -1,6 +1,6 @@ "use strict"; import { FaviconFeed, fetchIconFromRedirects } from "lib/FaviconFeed.sys.mjs"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; const FAKE_ENDPOINT = "https://foo.com/"; diff --git a/browser/components/newtab/test/unit/lib/NewTabInit.test.js b/browser/components/newtab/test/unit/lib/NewTabInit.test.js index 68ab9d7821..0def9293f0 100644 --- a/browser/components/newtab/test/unit/lib/NewTabInit.test.js +++ b/browser/components/newtab/test/unit/lib/NewTabInit.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { NewTabInit } from "lib/NewTabInit.sys.mjs"; describe("NewTabInit", () => { diff --git a/browser/components/newtab/test/unit/lib/PrefsFeed.test.js b/browser/components/newtab/test/unit/lib/PrefsFeed.test.js index 498c7198ab..8f33dce24f 100644 --- a/browser/components/newtab/test/unit/lib/PrefsFeed.test.js +++ b/browser/components/newtab/test/unit/lib/PrefsFeed.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; import { PrefsFeed } from "lib/PrefsFeed.sys.mjs"; diff --git a/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js b/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js index 9e68f4869a..05999be08d 100644 --- a/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js +++ b/browser/components/newtab/test/unit/lib/RecommendationProvider.test.js @@ -1,7 +1,4 @@ -import { - actionCreators as ac, - actionTypes as at, -} from "common/Actions.sys.mjs"; +import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { RecommendationProvider } from "lib/RecommendationProvider.sys.mjs"; import { combineReducers, createStore } from "redux"; import { reducers } from "common/Reducers.sys.mjs"; diff --git a/browser/components/newtab/test/unit/lib/SectionsManager.test.js b/browser/components/newtab/test/unit/lib/SectionsManager.test.js index b3a9abd70c..45c5b7c689 100644 --- a/browser/components/newtab/test/unit/lib/SectionsManager.test.js +++ b/browser/components/newtab/test/unit/lib/SectionsManager.test.js @@ -5,7 +5,7 @@ import { CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE, PRELOAD_MESSAGE_TYPE, -} from "common/Actions.sys.mjs"; +} from "common/Actions.mjs"; import { EventEmitter, GlobalOverrider } from "test/unit/utils"; import { SectionsFeed, SectionsManager } from "lib/SectionsManager.sys.mjs"; diff --git a/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js b/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js index a0789b182e..f5ba73d2ea 100644 --- a/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js +++ b/browser/components/newtab/test/unit/lib/SystemTickFeed.test.js @@ -2,7 +2,7 @@ import { SYSTEM_TICK_INTERVAL, SystemTickFeed, } from "lib/SystemTickFeed.sys.mjs"; -import { actionTypes as at } from "common/Actions.sys.mjs"; +import { actionTypes as at } from "common/Actions.mjs"; import { GlobalOverrider } from "test/unit/utils"; describe("System Tick Feed", () => { diff --git a/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js index 31a03947cd..1cb8a44631 100644 --- a/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js +++ b/browser/components/newtab/test/xpcshell/test_HighlightsFeed.js @@ -4,7 +4,7 @@ "use strict"; const { actionTypes: at } = ChromeUtils.importESModule( - "resource://activity-stream/common/Actions.sys.mjs" + "resource://activity-stream/common/Actions.mjs" ); ChromeUtils.defineESModuleGetters(this, { diff --git a/browser/components/newtab/test/xpcshell/test_PlacesFeed.js b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js index 19f9e343f5..78dda7818e 100644 --- a/browser/components/newtab/test/xpcshell/test_PlacesFeed.js +++ b/browser/components/newtab/test/xpcshell/test_PlacesFeed.js @@ -4,7 +4,7 @@ "use strict"; const { actionTypes: at, actionCreators: ac } = ChromeUtils.importESModule( - "resource://activity-stream/common/Actions.sys.mjs" + "resource://activity-stream/common/Actions.mjs" ); ChromeUtils.defineESModuleGetters(this, { diff --git a/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js index 59d82f5583..354eac8c2a 100644 --- a/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js +++ b/browser/components/newtab/test/xpcshell/test_TelemetryFeed.js @@ -4,11 +4,11 @@ "use strict"; const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule( - "resource://activity-stream/common/Actions.sys.mjs" + "resource://activity-stream/common/Actions.mjs" ); const { MESSAGE_TYPE_HASH: msg } = ChromeUtils.importESModule( - "resource:///modules/asrouter/ActorConstants.sys.mjs" + "resource:///modules/asrouter/ActorConstants.mjs" ); const { updateAppInfo } = ChromeUtils.importESModule( @@ -947,18 +947,18 @@ add_task( } ); -add_task(async function test_applyWhatsNewPolicy() { +add_task(async function test_applyToolbarBadgePolicy() { info( - "TelemetryFeed.applyWhatsNewPolicy should set client_id and set pingType" + "TelemetryFeed.applyToolbarBadgePolicy should set client_id and set pingType" ); let instance = new TelemetryFeed(); - let { ping, pingType } = await instance.applyWhatsNewPolicy({}); + let { ping, pingType } = await instance.applyToolbarBadgePolicy({}); Assert.equal( ping.client_id, Services.prefs.getCharPref("toolkit.telemetry.cachedClientID") ); - Assert.equal(pingType, "whats-new-panel"); + Assert.equal(pingType, "toolbar-badge"); }); add_task(async function test_applyInfoBarPolicy() { @@ -1288,10 +1288,10 @@ add_task(async function test_createASRouterEvent_call_correctPolicy() { message_id: "onboarding_message_01", }); - testCallCorrectPolicy("applyWhatsNewPolicy", { - action: "whats-new-panel_user_event", - event: "CLICK_BUTTON", - message_id: "whats-new-panel_message_01", + testCallCorrectPolicy("applyToolbarBadgePolicy", { + action: "badge_user_event", + event: "IMPRESSION", + message_id: "badge_message_01", }); testCallCorrectPolicy("applyMomentsPolicy", { @@ -2230,6 +2230,8 @@ add_task( const POS_1 = 1; const POS_2 = 4; const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ="; + const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20"); + const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30"); sandbox.stub(instance.sessions, "get").returns({ session_id: SESSION_ID }); let pingSubmitted = new Promise(resolve => { @@ -2252,6 +2254,14 @@ add_task( tile_id: String(2), }); Assert.equal(Glean.pocket.shim.testGetValue(), SHIM); + Assert.deepEqual( + Glean.pocket.fetchTimestamp.testGetValue(), + FETCH_TIMESTAMP + ); + Assert.deepEqual( + Glean.pocket.newtabCreationTimestamp.testGetValue(), + NEWTAB_CREATION_TIMESTAMP + ); resolve(); }); @@ -2272,10 +2282,12 @@ add_task( type: "spoc", recommendation_id: undefined, shim: SHIM, + fetchTimestamp: FETCH_TIMESTAMP.valueOf(), }, ], window_inner_width: 1000, window_inner_height: 900, + firstVisibleTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(), }); await pingSubmitted; @@ -2949,6 +2961,8 @@ add_task( Services.fog.testResetFOG(); const ACTION_POSITION = 42; const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ="; + const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20"); + const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30"); let action = ac.DiscoveryStreamUserEvent({ event: "CLICK", action_position: ACTION_POSITION, @@ -2957,6 +2971,8 @@ add_task( recommendation_id: undefined, tile_id: 448685088, shim: SHIM, + fetchTimestamp: FETCH_TIMESTAMP.valueOf(), + firstVisibleTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(), }, }); @@ -2966,6 +2982,14 @@ add_task( let pingSubmitted = new Promise(resolve => { GleanPings.spoc.testBeforeNextSubmit(reason => { Assert.equal(reason, "click"); + Assert.deepEqual( + Glean.pocket.fetchTimestamp.testGetValue(), + FETCH_TIMESTAMP + ); + Assert.deepEqual( + Glean.pocket.newtabCreationTimestamp.testGetValue(), + NEWTAB_CREATION_TIMESTAMP + ); resolve(); }); }); @@ -3043,6 +3067,8 @@ add_task( Services.fog.testResetFOG(); const ACTION_POSITION = 42; const SHIM = "Y29uc2lkZXIgeW91ciBjdXJpb3NpdHkgcmV3YXJkZWQ="; + const FETCH_TIMESTAMP = new Date("March 22, 2024 10:15:20"); + const NEWTAB_CREATION_TIMESTAMP = new Date("March 23, 2024 11:10:30"); let action = ac.DiscoveryStreamUserEvent({ event: "SAVE_TO_POCKET", action_position: ACTION_POSITION, @@ -3051,6 +3077,8 @@ add_task( recommendation_id: undefined, tile_id: 448685088, shim: SHIM, + fetchTimestamp: FETCH_TIMESTAMP.valueOf(), + newtabCreationTimestamp: NEWTAB_CREATION_TIMESTAMP.valueOf(), }, }); @@ -3064,6 +3092,14 @@ add_task( SHIM, "Pocket shim was recorded" ); + Assert.deepEqual( + Glean.pocket.fetchTimestamp.testGetValue(), + FETCH_TIMESTAMP + ); + Assert.deepEqual( + Glean.pocket.newtabCreationTimestamp.testGetValue(), + NEWTAB_CREATION_TIMESTAMP + ); resolve(); }); diff --git a/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js index 860e8758a5..4be520fcca 100644 --- a/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js +++ b/browser/components/newtab/test/xpcshell/test_TopSitesFeed.js @@ -8,7 +8,7 @@ const { TopSitesFeed, DEFAULT_TOP_SITES } = ChromeUtils.importESModule( ); const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule( - "resource://activity-stream/common/Actions.sys.mjs" + "resource://activity-stream/common/Actions.mjs" ); ChromeUtils.defineESModuleGetters(this, { diff --git a/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js b/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js new file mode 100644 index 0000000000..c6c12c17bf --- /dev/null +++ b/browser/components/newtab/test/xpcshell/test_WallpaperFeed.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { WallpaperFeed } = ChromeUtils.importESModule( + "resource://activity-stream/lib/WallpaperFeed.sys.mjs" +); + +const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule( + "resource://activity-stream/common/Actions.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + Utils: "resource://services-settings/Utils.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +const PREF_WALLPAPERS_ENABLED = + "browser.newtabpage.activity-stream.newtabWallpapers.enabled"; + +add_task(async function test_construction() { + let feed = new WallpaperFeed(); + + info("WallpaperFeed constructor should create initial values"); + + Assert.ok(feed, "Could construct a WallpaperFeed"); + Assert.ok(feed.loaded === false, "WallpaperFeed is not loaded"); + Assert.ok( + feed.wallpaperClient === "", + "wallpaperClient is initialized as an empty string" + ); + Assert.ok( + feed.wallpaperDB === "", + "wallpaperDB is initialized as an empty string" + ); + Assert.ok( + feed.baseAttachmentURL === "", + "baseAttachmentURL is initialized as an empty string" + ); +}); + +add_task(async function test_onAction_INIT() { + let sandbox = sinon.createSandbox(); + let feed = new WallpaperFeed(); + Services.prefs.setBoolPref(PREF_WALLPAPERS_ENABLED, true); + const attachment = { + attachment: { + location: "attachment", + }, + }; + sandbox.stub(feed, "RemoteSettings").returns({ + get: () => [attachment], + on: () => {}, + }); + sandbox.stub(Utils, "SERVER_URL").returns("http://localhost:8888/v1"); + feed.store = { + dispatch: sinon.spy(), + }; + sandbox.stub(feed, "fetch").resolves({ + json: () => ({ + capabilities: { + attachments: { + base_url: "http://localhost:8888/base_url/", + }, + }, + }), + }); + + info("WallpaperFeed.onAction INIT should initialize wallpapers"); + + await feed.onAction({ + type: at.INIT, + }); + + Assert.ok(feed.store.dispatch.calledOnce); + Assert.ok( + feed.store.dispatch.calledWith( + ac.BroadcastToContent({ + type: at.WALLPAPERS_SET, + data: [ + { + ...attachment, + wallpaperUrl: "http://localhost:8888/base_url/attachment", + }, + ], + meta: { + isStartup: true, + }, + }) + ) + ); + Services.prefs.clearUserPref(PREF_WALLPAPERS_ENABLED); + sandbox.restore(); +}); + +add_task(async function test_onAction_PREF_CHANGED() { + let sandbox = sinon.createSandbox(); + let feed = new WallpaperFeed(); + Services.prefs.setBoolPref(PREF_WALLPAPERS_ENABLED, true); + sandbox.stub(feed, "wallpaperSetup").returns(); + + info("WallpaperFeed.onAction PREF_CHANGED should call wallpaperSetup"); + + feed.onAction({ + type: at.PREF_CHANGED, + data: { name: "newtabWallpapers.enabled" }, + }); + + Assert.ok(feed.wallpaperSetup.calledOnce); + Assert.ok(feed.wallpaperSetup.calledWith(false)); + + Services.prefs.clearUserPref(PREF_WALLPAPERS_ENABLED); + sandbox.restore(); +}); diff --git a/browser/components/newtab/test/xpcshell/xpcshell.toml b/browser/components/newtab/test/xpcshell/xpcshell.toml index 87d73669d3..13c11b0541 100644 --- a/browser/components/newtab/test/xpcshell/xpcshell.toml +++ b/browser/components/newtab/test/xpcshell/xpcshell.toml @@ -26,3 +26,5 @@ support-files = ["../schemas/*.schema.json"] ["test_TopSitesFeed.js"] ["test_TopSitesFeed_glean.js"] + +["test_WallpaperFeed.js"] diff --git a/browser/components/newtab/webpack.system-addon.config.js b/browser/components/newtab/webpack.system-addon.config.js index a0400ec39e..68a384ea71 100644 --- a/browser/components/newtab/webpack.system-addon.config.js +++ b/browser/components/newtab/webpack.system-addon.config.js @@ -48,7 +48,7 @@ module.exports = (env = {}) => ({ }, // This resolve config allows us to import with paths relative to the root directory, e.g. "lib/ActivityStream.sys.mjs" resolve: { - extensions: [".js", ".jsx"], + extensions: [".js", ".jsx", ".mjs"], modules: ["node_modules", "."], }, externals: { diff --git a/browser/components/originattributes/test/browser/browser.toml b/browser/components/originattributes/test/browser/browser.toml index 5585b2a914..59c9d1c3c6 100644 --- a/browser/components/originattributes/test/browser/browser.toml +++ b/browser/components/originattributes/test/browser/browser.toml @@ -32,7 +32,7 @@ support-files = [ "file_thirdPartyChild.script.js", "file_thirdPartyChild.sharedworker.js", "file_thirdPartyChild.track.vtt", - "file_thirdPartyChild.video.ogv", + "file_thirdPartyChild.video.webm", "file_thirdPartyChild.worker.fetch.html", "file_thirdPartyChild.worker.js", "file_thirdPartyChild.worker.request.html", diff --git a/browser/components/originattributes/test/browser/browser_cache.js b/browser/components/originattributes/test/browser/browser_cache.js index ea8f0fe803..4c2369fc00 100644 --- a/browser/components/originattributes/test/browser/browser_cache.js +++ b/browser/components/originattributes/test/browser/browser_cache.js @@ -28,7 +28,7 @@ let suffixes = [ "xhr.html", "worker.xhr.html", "audio.ogg", - "video.ogv", + "video.webm", "track.vtt", "fetch.html", "worker.fetch.html", @@ -56,7 +56,7 @@ function cacheDataForContext(loadContextInfo) { return new Promise(resolve => { let cacheEntries = []; let cacheVisitor = { - onCacheStorageInfo(num, consumption) {}, + onCacheStorageInfo() {}, onCacheEntryInfo(uri, idEnhance) { cacheEntries.push({ uri, idEnhance }); }, @@ -176,7 +176,7 @@ async function doTest(aBrowser) { await SpecialPowers.spawn(aBrowser, [argObj], async function (arg) { content.windowUtils.clearSharedStyleSheetCache(); - let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.ogv"; + let videoURL = arg.urlPrefix + "file_thirdPartyChild.video.webm"; let audioURL = arg.urlPrefix + "file_thirdPartyChild.audio.ogg"; let trackURL = arg.urlPrefix + "file_thirdPartyChild.track.vtt"; let URLSuffix = "?r=" + arg.randomSuffix; @@ -257,7 +257,7 @@ async function doTest(aBrowser) { } // The check function, which checks the number of cache entries. -async function doCheck(aShouldIsolate, aInputA, aInputB) { +async function doCheck(aShouldIsolate) { let expectedEntryCount = 1; let data = []; data = data.concat( diff --git a/browser/components/originattributes/test/browser/browser_favicon_firstParty.js b/browser/components/originattributes/test/browser/browser_favicon_firstParty.js index 300c2f9f25..0a3bf39886 100644 --- a/browser/components/originattributes/test/browser/browser_favicon_firstParty.js +++ b/browser/components/originattributes/test/browser/browser_favicon_firstParty.js @@ -56,7 +56,7 @@ function clearAllPlacesFavicons() { return new Promise(resolve => { let observer = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic === "places-favicons-expired") { resolve(); Services.obs.removeObserver(observer, "places-favicons-expired"); @@ -77,7 +77,7 @@ function observeFavicon(aFirstPartyDomain, aExpectedCookie, aPageURI) { return new Promise(resolve => { let observer = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { // Make sure that the topic is 'http-on-modify-request'. if (aTopic === "http-on-modify-request") { // We check the firstPartyDomain for the originAttributes of the loading @@ -136,7 +136,7 @@ function observeFavicon(aFirstPartyDomain, aExpectedCookie, aPageURI) { function waitOnFaviconResponse(aFaviconURL) { return new Promise(resolve => { let observer = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if ( aTopic === "http-on-examine-response" || aTopic === "http-on-examine-cached-response" diff --git a/browser/components/originattributes/test/browser/browser_favicon_userContextId.js b/browser/components/originattributes/test/browser/browser_favicon_userContextId.js index 2f0a5d06a9..e8e687d938 100644 --- a/browser/components/originattributes/test/browser/browser_favicon_userContextId.js +++ b/browser/components/originattributes/test/browser/browser_favicon_userContextId.js @@ -57,7 +57,7 @@ function clearAllPlacesFavicons() { return new Promise(resolve => { let observer = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic === "places-favicons-expired") { resolve(); Services.obs.removeObserver(observer, "places-favicons-expired"); @@ -80,7 +80,7 @@ function FaviconObserver( } FaviconObserver.prototype = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { // Make sure that the topic is 'http-on-modify-request'. if (aTopic === "http-on-modify-request") { // We check the userContextId for the originAttributes of the loading diff --git a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js index 0266765782..450676eba2 100644 --- a/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js +++ b/browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js @@ -15,7 +15,7 @@ const TEST_ORIGIN = `http://${TEST_FIRST_PARTY}`; const TEST_BASE_PATH = "/browser/browser/components/originattributes/test/browser/"; const TEST_PATH = `${TEST_BASE_PATH}file_saveAs.sjs`; -const TEST_PATH_VIDEO = `${TEST_BASE_PATH}file_thirdPartyChild.video.ogv`; +const TEST_PATH_VIDEO = `${TEST_BASE_PATH}file_thirdPartyChild.video.webm`; const TEST_PATH_IMAGE = `${TEST_BASE_PATH}file_favicon.png`; // For the "Save Page As" test, we will check the channel of the sub-resource @@ -284,7 +284,7 @@ add_task(async function testPageInfoMediaSaveAs() { ); info("Open the media panel of the pageinfo."); - let pageInfo = BrowserPageInfo( + let pageInfo = BrowserCommands.pageInfo( gBrowser.selectedBrowser.currentURI.spec, "mediaTab" ); diff --git a/browser/components/originattributes/test/browser/browser_httpauth.js b/browser/components/originattributes/test/browser/browser_httpauth.js index b2e95e13ac..8821f3a92b 100644 --- a/browser/components/originattributes/test/browser/browser_httpauth.js +++ b/browser/components/originattributes/test/browser/browser_httpauth.js @@ -2,10 +2,6 @@ let { HttpServer } = ChromeUtils.importESModule( "resource://testing-common/httpd.sys.mjs" ); -let authPromptModalType = Services.prefs.getIntPref( - "prompts.modalType.httpAuth" -); - let server = new HttpServer(); server.registerPathHandler("/file.html", fileHandler); server.start(-1); @@ -57,7 +53,7 @@ function getResult() { return credentialQueue.shift(); } -async function doInit(aMode) { +async function doInit() { await SpecialPowers.pushPrefEnv({ set: [["privacy.partition.network_state", false]], }); diff --git a/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js b/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js index 34c77f746d..c826ec07d4 100644 --- a/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js +++ b/browser/components/originattributes/test/browser/browser_imageCacheIsolation.js @@ -72,12 +72,12 @@ async function doBefore() { } // the test function does nothing on purpose. -function doTest(aBrowser) { +function doTest() { return 0; } // the check function -function doCheck(shouldIsolate, a, b) { +function doCheck(shouldIsolate) { // if we're doing first party isolation and the image cache isolation is // working, then gHits should be 2 because the image would have been loaded // one per first party domain. if first party isolation is disabled, then diff --git a/browser/components/originattributes/test/browser/browser_sanitize.js b/browser/components/originattributes/test/browser/browser_sanitize.js index 61d236f249..0256a91eb7 100644 --- a/browser/components/originattributes/test/browser/browser_sanitize.js +++ b/browser/components/originattributes/test/browser/browser_sanitize.js @@ -20,8 +20,8 @@ function cacheDataForContext(loadContextInfo) { return new Promise(resolve => { let cachedURIs = []; let cacheVisitor = { - onCacheStorageInfo(num, consumption) {}, - onCacheEntryInfo(uri, idEnhance) { + onCacheStorageInfo() {}, + onCacheEntryInfo(uri) { cachedURIs.push(uri.asciiSpec); }, onCacheEntryVisitCompleted() { diff --git a/browser/components/originattributes/test/browser/file_saveAs.sjs b/browser/components/originattributes/test/browser/file_saveAs.sjs index 9b16250c76..8e1cc60dae 100644 --- a/browser/components/originattributes/test/browser/file_saveAs.sjs +++ b/browser/components/originattributes/test/browser/file_saveAs.sjs @@ -2,8 +2,8 @@ const HTTP_ORIGIN = "http://example.com"; const SECOND_ORIGIN = "http://example.org"; const URI_PATH = "/browser/browser/components/originattributes/test/browser/"; const LINK_PATH = `${URI_PATH}file_saveAs.sjs`; -// Reusing existing ogv file for testing. -const VIDEO_PATH = `${URI_PATH}file_thirdPartyChild.video.ogv`; +// Reusing existing webm file for testing. +const VIDEO_PATH = `${URI_PATH}file_thirdPartyChild.video.webm`; // Reusing existing png file for testing. const IMAGE_PATH = `${URI_PATH}file_favicon.png`; const FRAME_PATH = `${SECOND_ORIGIN}${URI_PATH}file_saveAs.sjs?image=1`; diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv b/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv Binary files differdeleted file mode 100644 index 68dee3cf2b..0000000000 --- a/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv +++ /dev/null diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.video.webm b/browser/components/originattributes/test/browser/file_thirdPartyChild.video.webm Binary files differnew file mode 100644 index 0000000000..5ad699fc1a --- /dev/null +++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.video.webm diff --git a/browser/components/originattributes/test/browser/head.js b/browser/components/originattributes/test/browser/head.js index bd4307fd1c..1d3dfd563e 100644 --- a/browser/components/originattributes/test/browser/head.js +++ b/browser/components/originattributes/test/browser/head.js @@ -448,7 +448,7 @@ this.IsolationTestTools = { // is finished before the next round of testing. if (SpecialPowers.useRemoteSubframes) { await new Promise(resolve => { - let observer = (subject, topic, data) => { + let observer = (subject, topic) => { if (topic === "ipc:content-shutdown") { Services.obs.removeObserver(observer, "ipc:content-shutdown"); resolve(); diff --git a/browser/components/pagedata/.eslintrc.js b/browser/components/pagedata/.eslintrc.js index 8ead689bcc..aac2436d20 100644 --- a/browser/components/pagedata/.eslintrc.js +++ b/browser/components/pagedata/.eslintrc.js @@ -5,8 +5,6 @@ "use strict"; module.exports = { - extends: ["plugin:mozilla/require-jsdoc"], - rules: { "mozilla/var-only-at-top-level": "error", "no-unused-expressions": "error", diff --git a/browser/components/pagedata/PageDataService.sys.mjs b/browser/components/pagedata/PageDataService.sys.mjs index 7160705c27..3cc93ead39 100644 --- a/browser/components/pagedata/PageDataService.sys.mjs +++ b/browser/components/pagedata/PageDataService.sys.mjs @@ -10,7 +10,6 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", - E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs", }); @@ -542,19 +541,8 @@ export const PageDataService = new (class PageDataService extends EventEmitter { this.#backgroundBrowsers.set(browser, resolve); let principal = Services.scriptSecurityManager.getSystemPrincipal(); - let oa = lazy.E10SUtils.predictOriginAttributes({ - browser, - }); let loadURIOptions = { triggeringPrincipal: principal, - remoteType: lazy.E10SUtils.getRemoteTypeForURI( - url, - true, - false, - lazy.E10SUtils.DEFAULT_REMOTE_TYPE, - null, - oa - ), }; browser.fixupAndLoadURIString(url, loadURIOptions); @@ -573,10 +561,8 @@ export const PageDataService = new (class PageDataService extends EventEmitter { * The notification's subject. * @param {string} topic * The notification topic. - * @param {string} data - * The data associated with the notification. */ - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "idle": lazy.logConsole.debug("User went idle"); diff --git a/browser/components/places/.eslintrc.js b/browser/components/places/.eslintrc.js deleted file mode 100644 index 9aafb4a214..0000000000 --- a/browser/components/places/.eslintrc.js +++ /dev/null @@ -1,9 +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/. */ - -"use strict"; - -module.exports = { - extends: ["plugin:mozilla/require-jsdoc"], -}; diff --git a/browser/components/places/content/places.js b/browser/components/places/content/places.js index 685fa12b51..9e2abaafcc 100644 --- a/browser/components/places/content/places.js +++ b/browser/components/places/content/places.js @@ -1168,7 +1168,7 @@ var ViewMenu = { menuitem.setAttribute("type", "radio"); menuitem.setAttribute("name", "columns"); // This column is the sort key. Its item is checked. - if (column.getAttribute("sortDirection") != "") { + if (column.hasAttribute("sortDirection")) { menuitem.setAttribute("checked", "true"); } } else if (type == "checkbox") { diff --git a/browser/components/places/content/places.xhtml b/browser/components/places/content/places.xhtml index e1ac09878b..d0e6a65eb5 100644 --- a/browser/components/places/content/places.xhtml +++ b/browser/components/places/content/places.xhtml @@ -15,6 +15,10 @@ onunload="PlacesOrganizer.destroy();" width="800" height="500" screenX="10" screenY="10" +#ifdef XP_MACOSX + drawtitle="true" + chromemargin="0,0,0,0" +#endif toggletoolbar="true" persist="width height screenX screenY sizemode"> diff --git a/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js b/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js index 16aeb08ad8..228fea654e 100644 --- a/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js +++ b/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js @@ -131,14 +131,14 @@ let checkContextMenu = async (cbfunc, optionItems, doc = document) => { if (expectedOptionItems.includes("placesContext_open")) { Assert.equal( doc.getElementById("placesContext_open").getAttribute("default"), - loadBookmarksInNewTab ? "" : "true", + loadBookmarksInNewTab ? null : "true", `placesContext_open has the correct "default" attribute when loadBookmarksInTabs = ${loadBookmarksInNewTab}` ); } if (expectedOptionItems.includes("placesContext_open:newtab")) { Assert.equal( doc.getElementById("placesContext_open:newtab").getAttribute("default"), - loadBookmarksInNewTab ? "true" : "", + loadBookmarksInNewTab ? "true" : null, `placesContext_open:newtab has the correct "default" attribute when loadBookmarksInTabs = ${loadBookmarksInNewTab}` ); } diff --git a/browser/components/places/tests/browser/browser_sidebarpanels_click.js b/browser/components/places/tests/browser/browser_sidebarpanels_click.js index 3e5b1c6ec6..9a1b039e78 100644 --- a/browser/components/places/tests/browser/browser_sidebarpanels_click.js +++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js @@ -157,16 +157,10 @@ function promiseAlertDialogObserved() { async function observer(subject) { info("alert dialog observed as expected"); Services.obs.removeObserver(observer, "common-dialog-loaded"); - Services.obs.removeObserver(observer, "tabmodal-dialog-loaded"); - if (subject.Dialog) { - subject.Dialog.ui.button0.click(); - } else { - subject.querySelector(".tabmodalprompt-button0").click(); - } + subject.Dialog.ui.button0.click(); resolve(); } Services.obs.addObserver(observer, "common-dialog-loaded"); - Services.obs.addObserver(observer, "tabmodal-dialog-loaded"); }); } diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js index bcd89bce15..5459e6f924 100644 --- a/browser/components/places/tests/browser/head.js +++ b/browser/components/places/tests/browser/head.js @@ -194,7 +194,7 @@ function promiseSetToolbarVisibility(aToolbar, aVisible) { function isToolbarVisible(aToolbar) { let hidingAttribute = aToolbar.getAttribute("type") == "menubar" ? "autohide" : "collapsed"; - let hidingValue = aToolbar.getAttribute(hidingAttribute).toLowerCase(); + let hidingValue = aToolbar.getAttribute(hidingAttribute)?.toLowerCase(); // Check for both collapsed="true" and collapsed="collapsed" return hidingValue !== "true" && hidingValue !== hidingAttribute; } diff --git a/browser/components/pocket/content/SaveToPocket.sys.mjs b/browser/components/pocket/content/SaveToPocket.sys.mjs index 60674acc82..9b9a30b4a2 100644 --- a/browser/components/pocket/content/SaveToPocket.sys.mjs +++ b/browser/components/pocket/content/SaveToPocket.sys.mjs @@ -101,7 +101,7 @@ export var SaveToPocket = { ); }, - observe(subject, topic, data) { + observe(subject, topic) { if (topic == "browser-delayed-startup-finished") { // We only get here if pocket is disabled; the observer is removed when // we're enabled. diff --git a/browser/components/pocket/content/panels/js/components/Home/Home.jsx b/browser/components/pocket/content/panels/js/components/Home/Home.jsx index 1036876725..8e15df1869 100644 --- a/browser/components/pocket/content/panels/js/components/Home/Home.jsx +++ b/browser/components/pocket/content/panels/js/components/Home/Home.jsx @@ -32,7 +32,7 @@ function Home(props) { : `` }`; - const loadingRecentSaves = useCallback(resp => { + const loadingRecentSaves = useCallback(() => { setArticlesState(prevState => ({ ...prevState, status: "loading", diff --git a/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx b/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx index 502c73b0a5..b750946234 100644 --- a/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx +++ b/browser/components/pocket/content/panels/js/components/Saved/Saved.jsx @@ -85,7 +85,7 @@ function Saved(props) { panelMessaging.addMessageListener( "PKT_getArticleInfoAttempted", - function (resp) { + function () { setArticleInfoAttempted(true); } ); diff --git a/browser/components/pocket/content/panels/js/home/overlay.jsx b/browser/components/pocket/content/panels/js/home/overlay.jsx index 4d49a09470..f8ee1578ba 100644 --- a/browser/components/pocket/content/panels/js/home/overlay.jsx +++ b/browser/components/pocket/content/panels/js/home/overlay.jsx @@ -7,7 +7,7 @@ import React from "react"; import ReactDOM from "react-dom"; import Home from "../components/Home/Home.jsx"; -var HomeOverlay = function (options) { +var HomeOverlay = function () { this.inited = false; this.active = false; }; diff --git a/browser/components/pocket/content/panels/js/main.bundle.js b/browser/components/pocket/content/panels/js/main.bundle.js index 36e2f82973..39a3dbccd7 100644 --- a/browser/components/pocket/content/panels/js/main.bundle.js +++ b/browser/components/pocket/content/panels/js/main.bundle.js @@ -275,7 +275,7 @@ function Home(props) { status: "" }); const utmParams = `utm_source=${utmSource}${utmCampaign && utmContent ? `&utm_campaign=${utmCampaign}&utm_content=${utmContent}` : ``}`; - const loadingRecentSaves = (0,react.useCallback)(resp => { + const loadingRecentSaves = (0,react.useCallback)(() => { setArticlesState(prevState => ({ ...prevState, status: "loading" @@ -381,7 +381,7 @@ It does not contain any logic for saving or communication with the extension or -var HomeOverlay = function (options) { +var HomeOverlay = function () { this.inited = false; this.active = false; }; @@ -487,7 +487,7 @@ It does not contain any logic for saving or communication with the extension or -var SignupOverlay = function (options) { +var SignupOverlay = function () { this.inited = false; this.active = false; this.create = function ({ @@ -772,7 +772,7 @@ function Saved(props) { messages.addMessageListener("PKT_articleInfoFetched", function (resp) { setSavedStoryState(resp?.data?.item_preview); }); - messages.addMessageListener("PKT_getArticleInfoAttempted", function (resp) { + messages.addMessageListener("PKT_getArticleInfoAttempted", function () { setArticleInfoAttempted(true); }); @@ -843,7 +843,7 @@ It does not contain any logic for saving or communication with the extension or -var SavedOverlay = function (options) { +var SavedOverlay = function () { this.inited = false; this.active = false; }; @@ -883,7 +883,7 @@ SavedOverlay.prototype = { -var StyleGuideOverlay = function (options) {}; +var StyleGuideOverlay = function () {}; StyleGuideOverlay.prototype = { create() { // TODO: Wrap popular topics component in JSX to work without needing an explicit container hierarchy for styling @@ -1072,7 +1072,7 @@ PKT_PANEL.prototype = { const config = { attributes: false, childList: true, subtree: true }; // Callback function to execute when mutations are observed - const callback = (mutationList, observer) => { + const callback = mutationList => { mutationList.forEach(mutation => { switch (mutation.type) { case "childList": { diff --git a/browser/components/pocket/content/panels/js/main.mjs b/browser/components/pocket/content/panels/js/main.mjs index b5ae0e9c3a..2c1da9528c 100644 --- a/browser/components/pocket/content/panels/js/main.mjs +++ b/browser/components/pocket/content/panels/js/main.mjs @@ -86,7 +86,7 @@ PKT_PANEL.prototype = { const config = { attributes: false, childList: true, subtree: true }; // Callback function to execute when mutations are observed - const callback = (mutationList, observer) => { + const callback = mutationList => { mutationList.forEach(mutation => { switch (mutation.type) { case "childList": { diff --git a/browser/components/pocket/content/panels/js/saved/overlay.jsx b/browser/components/pocket/content/panels/js/saved/overlay.jsx index ab2617f112..091821c149 100644 --- a/browser/components/pocket/content/panels/js/saved/overlay.jsx +++ b/browser/components/pocket/content/panels/js/saved/overlay.jsx @@ -7,7 +7,7 @@ import React from "react"; import ReactDOM from "react-dom"; import Saved from "../components/Saved/Saved.jsx"; -var SavedOverlay = function (options) { +var SavedOverlay = function () { this.inited = false; this.active = false; }; diff --git a/browser/components/pocket/content/panels/js/signup/overlay.jsx b/browser/components/pocket/content/panels/js/signup/overlay.jsx index 6143afbc83..ce3b681f15 100644 --- a/browser/components/pocket/content/panels/js/signup/overlay.jsx +++ b/browser/components/pocket/content/panels/js/signup/overlay.jsx @@ -8,7 +8,7 @@ import ReactDOM from "react-dom"; import pktPanelMessaging from "../messages.mjs"; import Signup from "../components/Signup/Signup.jsx"; -var SignupOverlay = function (options) { +var SignupOverlay = function () { this.inited = false; this.active = false; diff --git a/browser/components/pocket/content/panels/js/style-guide/overlay.jsx b/browser/components/pocket/content/panels/js/style-guide/overlay.jsx index fbc0dac069..b802a9159b 100644 --- a/browser/components/pocket/content/panels/js/style-guide/overlay.jsx +++ b/browser/components/pocket/content/panels/js/style-guide/overlay.jsx @@ -6,7 +6,7 @@ import Button from "../components/Button/Button.jsx"; import PopularTopics from "../components/PopularTopics/PopularTopics.jsx"; import TagPicker from "../components/TagPicker/TagPicker.jsx"; -var StyleGuideOverlay = function (options) {}; +var StyleGuideOverlay = function () {}; StyleGuideOverlay.prototype = { create() { diff --git a/browser/components/pocket/content/pktApi.sys.mjs b/browser/components/pocket/content/pktApi.sys.mjs index 16d0948b36..132f9369ce 100644 --- a/browser/components/pocket/content/pktApi.sys.mjs +++ b/browser/components/pocket/content/pktApi.sys.mjs @@ -47,7 +47,6 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { IndexedDB: "resource://gre/modules/IndexedDB.sys.mjs", - NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", }); @@ -291,12 +290,12 @@ export var pktApi = (function () { "extensions.pocket.oAuthConsumerKey" ); } else { - baseAPIUrl = `https://${lazy.NimbusFeatures.saveToPocket.getVariable( - "bffApi" + baseAPIUrl = `https://${Services.prefs.getStringPref( + "extensions.pocket.bffApi" )}/desktop/v1`; - oAuthConsumerKey = lazy.NimbusFeatures.saveToPocket.getVariable( - "oAuthConsumerKeyBff" + oAuthConsumerKey = Services.prefs.getStringPref( + "extensions.pocket.oAuthConsumerKeyBff" ); } @@ -309,7 +308,7 @@ export var pktApi = (function () { data.locale_lang = Services.locale.appLocaleAsBCP47; data.consumer_key = oAuthConsumerKey; - var request = new XMLHttpRequest(); + var request = new XMLHttpRequest({ mozAnon: false }); if (!useBFF) { request.open("POST", url, true); @@ -317,7 +316,7 @@ export var pktApi = (function () { request.open("GET", url, true); } - request.onreadystatechange = function (e) { + request.onreadystatechange = function () { if (request.readyState == 4) { // "done" is a completed XHR regardless of success/error: if (options.done) { @@ -487,7 +486,7 @@ export var pktApi = (function () { access_token: getAccessToken(), url, }, - success(data) { + success() { if (options.success) { options.success.apply(options, Array.apply(null, arguments)); } @@ -508,7 +507,7 @@ export var pktApi = (function () { data: { access_token: getAccessToken(), }, - success(data) { + success() { if (options.success) { options.success.apply(options, Array.apply(null, arguments)); } @@ -761,8 +760,9 @@ export var pktApi = (function () { access_token: getAccessToken(), }); - const useBFF = - lazy.NimbusFeatures.saveToPocket.getVariable("bffRecentSaves"); + const useBFF = Services.prefs.getBoolPref( + "extensions.pocket.bffRecentSaves" + ); return apiRequest( { @@ -816,8 +816,9 @@ export var pktApi = (function () { { count: 4 }, { success(data) { - const useBFF = - lazy.NimbusFeatures.saveToPocket.getVariable("bffRecentSaves"); + const useBFF = Services.prefs.getBoolPref( + "extensions.pocket.bffRecentSaves" + ); // Don't try to parse bad or missing data if ( diff --git a/browser/components/pocket/content/pktUI.js b/browser/components/pocket/content/pktUI.js index 60b7e3bcc3..05e31b5c30 100644 --- a/browser/components/pocket/content/pktUI.js +++ b/browser/components/pocket/content/pktUI.js @@ -46,7 +46,6 @@ ChromeUtils.defineESModuleGetters(this, { ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", - NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", pktApi: "chrome://pocket/content/pktApi.sys.mjs", pktTelemetry: "chrome://pocket/content/pktTelemetry.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", @@ -125,11 +124,13 @@ var pktUI = (function () { * Show the sign-up panel */ function showSignUp() { - getFirefoxAccountSignedInUser(function (userdata) { + getFirefoxAccountSignedInUser(function () { showPanel( "about:pocket-signup?" + "emailButton=" + - NimbusFeatures.saveToPocket.getVariable("emailButton"), + Services.prefs.getBoolPref( + "extensions.pocket.refresh.emailButton.enabled" + ), `signup` ); }); @@ -154,8 +155,9 @@ var pktUI = (function () { * Show the Pocket home panel state */ function showPocketHome() { - const hideRecentSaves = - NimbusFeatures.saveToPocket.getVariable("hideRecentSaves"); + const hideRecentSaves = Services.prefs.getBoolPref( + "extensions.pocket.refresh.hideRecentSaves.enabled" + ); const locale = getUILocale(); let panel = `home_no_topics`; if (locale.startsWith("en-")) { @@ -232,7 +234,11 @@ var pktUI = (function () { async function onShowHome() { pktTelemetry.submitPocketButtonPing("click", "home_button"); - if (!NimbusFeatures.saveToPocket.getVariable("hideRecentSaves")) { + if ( + !Services.prefs.getBoolPref( + "extensions.pocket.refresh.hideRecentSaves.enabled" + ) + ) { let recentSaves = await pktApi.getRecentSavesCache(); if (recentSaves) { // We have cache, so we can use those. @@ -284,7 +290,7 @@ var pktUI = (function () { // Add url var options = { - success(data, request) { + success(data) { var item = data.item; var ho2 = data.ho2; var accountState = data.account_state; @@ -299,7 +305,11 @@ var pktUI = (function () { pktUIMessaging.sendMessageToPanel(saveLinkMessageId, successResponse); SaveToPocket.itemSaved(); - if (!NimbusFeatures.saveToPocket.getVariable("hideRecentSaves")) { + if ( + !Services.prefs.getBoolPref( + "extensions.pocket.refresh.hideRecentSaves.enabled" + ) + ) { // Articles saved for the first time (by anyone) won't have a resolved_id if (item?.resolved_id && item?.resolved_id !== "0") { pktApi.getArticleInfo(item.resolved_url, { @@ -493,7 +503,7 @@ var pktUI = (function () { .then(userData => { callback(userData); }) - .then(null, error => { + .then(null, () => { callback(); }); } diff --git a/browser/components/pocket/test/browser_pocket_button_icon_state.js b/browser/components/pocket/test/browser_pocket_button_icon_state.js index c2cba8133b..65c1608b9d 100644 --- a/browser/components/pocket/test/browser_pocket_button_icon_state.js +++ b/browser/components/pocket/test/browser_pocket_button_icon_state.js @@ -72,10 +72,14 @@ function checkPanelClosed() { let pocketButton = document.getElementById("save-to-pocket-button"); // Something should have closed the Pocket panel, icon should no longer be red. is(pocketButton.open, false, "Pocket button is closed"); - is(pocketButton.getAttribute("pocketed"), "", "Pocket item is not pocketed"); + is( + pocketButton.getAttribute("pocketed"), + null, + "Pocket item is not pocketed" + ); } -test_runner(async function test_pocketButtonState_changeTabs({ sandbox }) { +test_runner(async function test_pocketButtonState_changeTabs() { let tab = await BrowserTestUtils.openNewForegroundTab( gBrowser, "https://example.com/browser/browser/components/pocket/test/test.html" @@ -101,7 +105,7 @@ test_runner(async function test_pocketButtonState_changeTabs({ sandbox }) { BrowserTestUtils.removeTab(tab); }); -test_runner(async function test_pocketButtonState_changeLocation({ sandbox }) { +test_runner(async function test_pocketButtonState_changeLocation() { let tab = await BrowserTestUtils.openNewForegroundTab( gBrowser, "https://example.com/browser/browser/components/pocket/test/test.html" diff --git a/browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js b/browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js index 5abe3b3db1..4b6cc8f7ad 100644 --- a/browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js +++ b/browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js @@ -78,9 +78,7 @@ test_runner(async function test_AboutPocketParent_sendResponseMessageToPanel({ }); test_runner( - async function test_AboutPocketParent_receiveMessage_PKT_show_signup({ - sandbox, - }) { + async function test_AboutPocketParent_receiveMessage_PKT_show_signup() { await aboutPocketParent.receiveMessage({ name: "PKT_show_signup", }); @@ -96,9 +94,7 @@ test_runner( ); test_runner( - async function test_AboutPocketParent_receiveMessage_PKT_show_saved({ - sandbox, - }) { + async function test_AboutPocketParent_receiveMessage_PKT_show_saved() { await aboutPocketParent.receiveMessage({ name: "PKT_show_saved", }); @@ -113,9 +109,7 @@ test_runner( } ); -test_runner(async function test_AboutPocketParent_receiveMessage_PKT_close({ - sandbox, -}) { +test_runner(async function test_AboutPocketParent_receiveMessage_PKT_close() { await aboutPocketParent.receiveMessage({ name: "PKT_close", }); @@ -130,9 +124,7 @@ test_runner(async function test_AboutPocketParent_receiveMessage_PKT_close({ }); test_runner( - async function test_AboutPocketParent_receiveMessage_PKT_openTabWithUrl({ - sandbox, - }) { + async function test_AboutPocketParent_receiveMessage_PKT_openTabWithUrl() { await aboutPocketParent.receiveMessage({ name: "PKT_openTabWithUrl", data: { foo: 1 }, @@ -155,9 +147,7 @@ test_runner( ); test_runner( - async function test_AboutPocketParent_receiveMessage_PKT_openTabWithPocketUrl({ - sandbox, - }) { + async function test_AboutPocketParent_receiveMessage_PKT_openTabWithPocketUrl() { await aboutPocketParent.receiveMessage({ name: "PKT_openTabWithPocketUrl", data: { foo: 1 }, diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js index c30a51c67c..0cd498fd96 100644 --- a/browser/components/preferences/preferences.js +++ b/browser/components/preferences/preferences.js @@ -276,22 +276,6 @@ function init_all() { }); } -function telemetryBucketForCategory(category) { - category = category.toLowerCase(); - switch (category) { - case "containers": - case "general": - case "home": - case "privacy": - case "search": - case "sync": - case "searchresults": - return category; - default: - return "unknown"; - } -} - function onHashChange() { gotoPref(null, "hash"); } @@ -454,16 +438,6 @@ function search(aQuery, aAttribute) { } element.classList.remove("visually-hidden"); } - - let keysets = mainPrefPane.getElementsByTagName("keyset"); - for (let element of keysets) { - let attributeValue = element.getAttribute(aAttribute); - if (attributeValue == aQuery) { - element.removeAttribute("disabled"); - } else { - element.setAttribute("disabled", true); - } - } } async function spotlight(subcategory, category) { diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml index eee227822a..64062f1f77 100644 --- a/browser/components/preferences/preferences.xhtml +++ b/browser/components/preferences/preferences.xhtml @@ -85,6 +85,8 @@ <script type="module" src="chrome://global/content/elements/moz-toggle.mjs"/> <script type="module" src="chrome://global/content/elements/moz-message-bar.mjs" /> <script type="module" src="chrome://global/content/elements/moz-label.mjs"/> + <script type="module" src="chrome://global/content/elements/moz-card.mjs"></script> + <script type="module" src="chrome://global/content/elements/moz-button.mjs"></script> </head> <html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" @@ -224,7 +226,7 @@ <hbox class="sticky-inner-container" pack="end" align="start"> <hbox id="policies-container" class="info-box-container smaller-font-size" flex="1" hidden="true"> <hbox class="info-icon-container"> - <html:img class="info-icon"></html:img> + <html:img class="info-icon" data-l10n-attrs="alt" data-l10n-id="managed-notice-info-icon"></html:img> </hbox> <hbox align="center" flex="1"> <html:a href="about:policies" target="_blank" data-l10n-id="managed-notice"/> diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml index 224a5f5cbb..5c45771bc2 100644 --- a/browser/components/preferences/privacy.inc.xhtml +++ b/browser/components/preferences/privacy.inc.xhtml @@ -1182,21 +1182,18 @@ <label class="doh-status-label" id="dohResolver"/> <label class="doh-status-label" id="dohSteeringStatus" data-l10n-id="preferences-doh-steering-status" hidden="true"/> </vbox> - <hbox id="dohExceptionBox"> - <label flex="1" data-l10n-id="preferences-doh-exceptions-description"/> - <button id="dohExceptionsButton" - is="highlightable-button" - class="accessory-button" - data-l10n-id="preferences-doh-manage-exceptions" - search-l10n-ids=" - permissions-doh-entry-field, - permissions-doh-add-exception.label, - permissions-doh-remove.label, - permissions-doh-remove-all.label, - permissions-exceptions-doh-window.title, - permissions-exceptions-manage-doh-desc, - "/> - </hbox> + <button id="dohExceptionsButton" + is="highlightable-button" + class="accessory-button" + data-l10n-id="preferences-doh-manage-exceptions" + search-l10n-ids=" + permissions-doh-entry-field, + permissions-doh-add-exception.label, + permissions-doh-remove.label, + permissions-doh-remove-all.label, + permissions-exceptions-doh-window.title, + permissions-exceptions-manage-doh-desc, + "/> <vbox> <label><html:h2 id="dohGroupMessage" data-l10n-id="preferences-doh-group-message2"/></label> <vbox id="dohCategories"> diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js index 3b07b9cabf..89fed04e21 100644 --- a/browser/components/preferences/privacy.js +++ b/browser/components/preferences/privacy.js @@ -60,6 +60,13 @@ ChromeUtils.defineLazyGetter(this, "AlertsServiceDND", function () { } }); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gParentalControlsService", + "@mozilla.org/parental-controls-service;1", + "nsIParentalControlsService" +); + XPCOMUtils.defineLazyPreferenceGetter( this, "OS_AUTH_ENABLED", @@ -682,11 +689,14 @@ var gPrivacyPane = { function computeStatus() { let mode = Services.dns.currentTrrMode; - let confirmationState = Services.dns.currentTrrConfirmationState; if ( mode == Ci.nsIDNSService.MODE_TRRFIRST || mode == Ci.nsIDNSService.MODE_TRRONLY ) { + if (lazy.gParentalControlsService.parentalControlsEnabled) { + return "preferences-doh-status-not-active"; + } + let confirmationState = Services.dns.currentTrrConfirmationState; switch (confirmationState) { case Ci.nsIDNSService.CONFIRM_TRYING_OK: case Ci.nsIDNSService.CONFIRM_OK: @@ -702,7 +712,16 @@ var gPrivacyPane = { let errReason = ""; let confirmationStatus = Services.dns.lastConfirmationStatus; - if (confirmationStatus != Cr.NS_OK) { + let mode = Services.dns.currentTrrMode; + if ( + (mode == Ci.nsIDNSService.MODE_TRRFIRST || + mode == Ci.nsIDNSService.MODE_TRRONLY) && + lazy.gParentalControlsService.parentalControlsEnabled + ) { + errReason = Services.dns.getTRRSkipReasonName( + Ci.nsITRRSkipReason.TRR_PARENTAL_CONTROL + ); + } else if (confirmationStatus != Cr.NS_OK) { errReason = ChromeUtils.getXPCOMErrorName(confirmationStatus); } else { errReason = Services.dns.getTRRSkipReasonName( diff --git a/browser/components/preferences/sync.inc.xhtml b/browser/components/preferences/sync.inc.xhtml index d3af690b93..492491a369 100644 --- a/browser/components/preferences/sync.inc.xhtml +++ b/browser/components/preferences/sync.inc.xhtml @@ -22,7 +22,7 @@ <description id="noFxaDescription" class="description-deemphasized" flex="1" data-l10n-id="sync-signedout-description2"/> </vbox> <vbox> - <image class="fxaSyncIllustration"/> + <image class="fxaSyncIllustration" alt=""/> </vbox> </hbox> <hbox id="fxaNoLoginStatus" align="center" flex="1"> @@ -37,6 +37,7 @@ </hbox> <label class="fxaMobilePromo" data-l10n-id="sync-mobile-promo"> <html:img + role="none" src="chrome://browser/skin/logo-android.svg" data-l10n-name="android-icon" class="androidIcon"/> @@ -44,6 +45,7 @@ data-l10n-name="android-link" class="fxaMobilePromo-android text-link" target="_blank"/> <html:img + role="none" src="chrome://browser/skin/logo-ios.svg" data-l10n-name="ios-icon" class="iOSIcon"/> @@ -66,7 +68,8 @@ <image id="openChangeProfileImage" class="fxaProfileImage actionable" role="button" - data-l10n-id="sync-profile-picture"/> + data-l10n-attrs="alt" + data-l10n-id="sync-profile-picture-with-alt"/> <vbox flex="1" pack="center"> <hbox flex="1" align="baseline"> <label id="fxaDisplayName" hidden="true"> @@ -88,11 +91,15 @@ <!-- logged in to an unverified account --> <hbox id="fxaLoginUnverified"> <vbox> - <image class="fxaProfileImage"/> + <image class="fxaProfileImage" + data-l10n-attrs="alt" + data-l10n-id="sync-profile-picture-account-problem"/> </vbox> <vbox flex="1" pack="center"> <hbox align="center"> - <image class="fxaLoginRejectedWarning"/> + <image class="fxaLoginRejectedWarning" + data-l10n-attrs="alt" + data-l10n-id="fxa-login-rejected-warning"/> <description flex="1" class="l10nArgsEmailAddress" data-l10n-id="sync-signedin-unverified" @@ -112,11 +119,15 @@ <!-- logged in locally but server rejected credentials --> <hbox id="fxaLoginRejected"> <vbox> - <image class="fxaProfileImage"/> + <image class="fxaProfileImage" + data-l10n-attrs="alt" + data-l10n-id="sync-profile-picture-account-problem"/> </vbox> <vbox flex="1" pack="center"> <hbox align="center"> - <image class="fxaLoginRejectedWarning"/> + <image class="fxaLoginRejectedWarning" + data-l10n-attrs="alt" + data-l10n-id="fxa-login-rejected-warning"/> <description flex="1" class="l10nArgsEmailAddress" data-l10n-id="sync-signedin-login-failure" @@ -187,35 +198,35 @@ <label data-l10n-id="sync-syncing-across-devices-heading"/> <html:div class="sync-engines-list"> <html:div engine_preference="services.sync.engine.bookmarks"> - <image class="sync-engine-image sync-engine-bookmarks"/> + <image class="sync-engine-image sync-engine-bookmarks" alt=""/> <label data-l10n-id="sync-currently-syncing-bookmarks"/> </html:div> <html:div engine_preference="services.sync.engine.history"> - <image class="sync-engine-image sync-engine-history"/> + <image class="sync-engine-image sync-engine-history" alt=""/> <label data-l10n-id="sync-currently-syncing-history"/> </html:div> <html:div engine_preference="services.sync.engine.tabs"> - <image class="sync-engine-image sync-engine-tabs"/> + <image class="sync-engine-image sync-engine-tabs" alt=""/> <label data-l10n-id="sync-currently-syncing-tabs"/> </html:div> <html:div engine_preference="services.sync.engine.passwords"> - <image class="sync-engine-image sync-engine-passwords"/> + <image class="sync-engine-image sync-engine-passwords" alt=""/> <label data-l10n-id="sync-currently-syncing-passwords"/> </html:div> <html:div engine_preference="services.sync.engine.addresses"> - <image class="sync-engine-image sync-engine-addresses"/> + <image class="sync-engine-image sync-engine-addresses" alt=""/> <label data-l10n-id="sync-currently-syncing-addresses"/> </html:div> <html:div engine_preference="services.sync.engine.creditcards"> - <image class="sync-engine-image sync-engine-creditcards"/> + <image class="sync-engine-image sync-engine-creditcards" alt=""/> <label data-l10n-id="sync-currently-syncing-payment-methods"/> </html:div> <html:div engine_preference="services.sync.engine.addons"> - <image class="sync-engine-image sync-engine-addons"/> + <image class="sync-engine-image sync-engine-addons" alt=""/> <label data-l10n-id="sync-currently-syncing-addons"/> </html:div> <html:div engine_preference="services.sync.engine.prefs"> - <image class="sync-engine-image sync-engine-prefs"/> + <image class="sync-engine-image sync-engine-prefs" alt=""/> <label data-l10n-id="sync-currently-syncing-settings"/> </html:div> </html:div> diff --git a/browser/components/preferences/tests/browser.toml b/browser/components/preferences/tests/browser.toml index 9e619ce4be..523110dbc9 100644 --- a/browser/components/preferences/tests/browser.toml +++ b/browser/components/preferences/tests/browser.toml @@ -10,6 +10,11 @@ support-files = [ "addons/set_homepage.xpi", "addons/set_newtab.xpi", ] +skip-if = [ + "os == 'linux' && os_version == '18.04' && asan", # manifest runs too long + "os == 'linux' && os_version == '18.04' && tsan", # manifest runs too long + "win11_2009 && asan", # manifest runs too long +] ["browser_about_settings.js"] @@ -276,7 +281,6 @@ support-files = [ "subdialog.xhtml", "subdialog2.xhtml", ] -fail-if = ["a11y_checks"] # Bug 1854636 clicked label.dialogTitle, vbox#dialogTemplate.dialogOverlay may not be focusable ["browser_sync_chooseWhatToSync.js"] diff --git a/browser/components/preferences/tests/browser_applications_selection.js b/browser/components/preferences/tests/browser_applications_selection.js index 683ce76a89..23f0e00af8 100644 --- a/browser/components/preferences/tests/browser_applications_selection.js +++ b/browser/components/preferences/tests/browser_applications_selection.js @@ -335,10 +335,12 @@ add_task(async function sortingCheck() { "Number of items should not change." ); for (let i = 0; i < siteItems.length - 1; ++i) { - let aType = siteItems[i].getAttribute("actionDescription").toLowerCase(); - let bType = siteItems[i + 1] - .getAttribute("actionDescription") - .toLowerCase(); + let aType = ( + siteItems[i].getAttribute("actionDescription") || "" + ).toLowerCase(); + let bType = ( + siteItems[i + 1].getAttribute("actionDescription") || "" + ).toLowerCase(); let result = 0; if (aType > bType) { result = 1; @@ -375,10 +377,12 @@ add_task(async function sortingCheck() { "Number of items should not change." ); for (let i = 0; i < siteItems.length - 1; ++i) { - let aType = siteItems[i].getAttribute("typeDescription").toLowerCase(); - let bType = siteItems[i + 1] - .getAttribute("typeDescription") - .toLowerCase(); + let aType = ( + siteItems[i].getAttribute("typeDescription") || "" + ).toLowerCase(); + let bType = ( + siteItems[i + 1].getAttribute("typeDescription") || "" + ).toLowerCase(); let result = 0; if (aType > bType) { result = 1; diff --git a/browser/components/preferences/tests/browser_contentblocking.js b/browser/components/preferences/tests/browser_contentblocking.js index 3d33f2ed7d..c178233a72 100644 --- a/browser/components/preferences/tests/browser_contentblocking.js +++ b/browser/components/preferences/tests/browser_contentblocking.js @@ -1021,7 +1021,7 @@ add_task(async function testDisableTPCheckBoxDisablesEmailTP() { // Verify the checkbox is unchecked after clicking. is( tpCheckbox.getAttribute("checked"), - "", + null, "Tracking protection checkbox is unchecked" ); diff --git a/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js index 48469cfce4..ebe9c41127 100644 --- a/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js +++ b/browser/components/preferences/tests/browser_privacy_dnsoverhttps.js @@ -16,6 +16,10 @@ ChromeUtils.defineESModuleGetters(this, { DoHTestUtils: "resource://testing-common/DoHTestUtils.sys.mjs", }); +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + const TRR_MODE_PREF = "network.trr.mode"; const TRR_URI_PREF = "network.trr.uri"; const TRR_CUSTOM_URI_PREF = "network.trr.custom_uri"; @@ -106,6 +110,164 @@ function waitForPrefObserver(name) { }); } +// Mock parental controls service in order to enable it +let parentalControlsService = { + parentalControlsEnabled: true, + QueryInterface: ChromeUtils.generateQI(["nsIParentalControlsService"]), +}; +let mockParentalControlsServiceCid = undefined; + +async function setMockParentalControlEnabled(aEnabled) { + if (mockParentalControlsServiceCid != undefined) { + MockRegistrar.unregister(mockParentalControlsServiceCid); + mockParentalControlsServiceCid = undefined; + } + if (aEnabled) { + mockParentalControlsServiceCid = MockRegistrar.register( + "@mozilla.org/parental-controls-service;1", + parentalControlsService + ); + } + Services.dns.reloadParentalControlEnabled(); +} + +add_task(async function testParentalControls() { + async function withConfiguration(configuration, fn) { + info("testParentalControls"); + + await resetPrefs(); + Services.prefs.setIntPref(TRR_MODE_PREF, configuration.trr_mode); + await setMockParentalControlEnabled(configuration.parentalControlsState); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + let doc = gBrowser.selectedBrowser.contentDocument; + let statusElement = doc.getElementById("dohStatus"); + + await TestUtils.waitForCondition(() => { + return ( + document.l10n.getAttributes(statusElement).args.status == + configuration.wait_for_doh_status + ); + }); + + await fn({ + statusElement, + }); + + gBrowser.removeCurrentTab(); + await setMockParentalControlEnabled(false); + } + + info("Check parental controls disabled, TRR off"); + await withConfiguration( + { + parentalControlsState: false, + trr_mode: 0, + wait_for_doh_status: "Off", + }, + async res => { + is( + document.l10n.getAttributes(res.statusElement).args.status, + "Off", + "expecting status off" + ); + } + ); + + info("Check parental controls enabled, TRR off"); + await withConfiguration( + { + parentalControlsState: true, + trr_mode: 0, + wait_for_doh_status: "Off", + }, + async res => { + is( + document.l10n.getAttributes(res.statusElement).args.status, + "Off", + "expecting status off" + ); + } + ); + + // Enable the rollout. + await DoHTestUtils.loadRemoteSettingsConfig({ + providers: "example", + rolloutEnabled: true, + steeringEnabled: false, + steeringProviders: "", + autoDefaultEnabled: false, + autoDefaultProviders: "", + id: "global", + }); + + info("Check parental controls disabled, TRR first"); + await withConfiguration( + { + parentalControlsState: false, + trr_mode: 2, + wait_for_doh_status: "Active", + }, + async res => { + is( + document.l10n.getAttributes(res.statusElement).args.status, + "Active", + "expecting status active" + ); + } + ); + + info("Check parental controls enabled, TRR first"); + await withConfiguration( + { + parentalControlsState: true, + trr_mode: 2, + wait_for_doh_status: "Not active (TRR_PARENTAL_CONTROL)", + }, + async res => { + is( + document.l10n.getAttributes(res.statusElement).args.status, + "Not active (TRR_PARENTAL_CONTROL)", + "expecting status not active" + ); + } + ); + + info("Check parental controls disabled, TRR only"); + await withConfiguration( + { + parentalControlsState: false, + trr_mode: 3, + wait_for_doh_status: "Active", + }, + async res => { + is( + document.l10n.getAttributes(res.statusElement).args.status, + "Active", + "expecting status active" + ); + } + ); + + info("Check parental controls enabled, TRR only"); + await withConfiguration( + { + parentalControlsState: true, + trr_mode: 3, + wait_for_doh_status: "Not active (TRR_PARENTAL_CONTROL)", + }, + async res => { + is( + document.l10n.getAttributes(res.statusElement).args.status, + "Not active (TRR_PARENTAL_CONTROL)", + "expecting status not active" + ); + } + ); + + await resetPrefs(); +}); + async function testWithProperties(props, startTime) { info( Date.now() - diff --git a/browser/components/preferences/tests/browser_subdialogs.js b/browser/components/preferences/tests/browser_subdialogs.js index 8763ae9146..b604ac0a7f 100644 --- a/browser/components/preferences/tests/browser_subdialogs.js +++ b/browser/components/preferences/tests/browser_subdialogs.js @@ -173,7 +173,7 @@ async function close_subdialog_and_test_generic_end_state( ); Assert.equal( frame.getAttribute("style"), - "", + null, "inline styles should be cleared" ); Assert.equal( @@ -407,17 +407,29 @@ add_task(async function background_click_should_close_dialog() { // Clicking on an inactive part of dialog itself should not close the dialog. // Click the dialog title bar here to make sure nothing happens. info("clicking the dialog title bar"); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to confirm the opened + // dialog won't be dismissed. It is not meant to be interactive and is not + // expected to be accessible, therefore this rule check shall be ignored by + // a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); BrowserTestUtils.synthesizeMouseAtCenter( ".dialogTitle", {}, tab.linkedBrowser ); + AccessibilityUtils.resetEnv(); // Close the dialog by clicking on the overlay background. Simulate a click // at point (2,2) instead of (0,0) so we are sure we're clicking on the // overlay background instead of some boundary condition that a real user // would never click. info("clicking the overlay background"); + // We intentionally turn off this a11y check, because the following click + // is purposefully targeting a non-interactive element to dismiss the opened + // dialog with a mouse which can be done by assistive technology and keyboard + // by pressing `Esc` key, this rule check shall be ignored by a11y_checks. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); await close_subdialog_and_test_generic_end_state( tab.linkedBrowser, function () { @@ -432,6 +444,7 @@ add_task(async function background_click_should_close_dialog() { 0, { runClosingFnOutsideOfContentTask: true } ); + AccessibilityUtils.resetEnv(); }); add_task(async function escape_should_close_dialog() { diff --git a/browser/components/preferences/tests/siteData/browser.toml b/browser/components/preferences/tests/siteData/browser.toml index 9f4f8306e1..b7e6ba1b6d 100644 --- a/browser/components/preferences/tests/siteData/browser.toml +++ b/browser/components/preferences/tests/siteData/browser.toml @@ -10,6 +10,8 @@ support-files = [ ["browser_clearSiteData.js"] +["browser_clearSiteData_v2.js"] + ["browser_siteData.js"] ["browser_siteData2.js"] diff --git a/browser/components/preferences/tests/siteData/browser_clearSiteData.js b/browser/components/preferences/tests/siteData/browser_clearSiteData.js index 7ae1fda453..4924dccfea 100644 --- a/browser/components/preferences/tests/siteData/browser_clearSiteData.js +++ b/browser/components/preferences/tests/siteData/browser_clearSiteData.js @@ -7,10 +7,6 @@ const { PermissionTestUtils } = ChromeUtils.importESModule( "resource://testing-common/PermissionTestUtils.sys.mjs" ); -let useOldClearHistoryDialog = Services.prefs.getBoolPref( - "privacy.sanitize.useOldClearHistoryDialog" -); - async function testClearData(clearSiteData, clearCache) { PermissionTestUtils.add( TEST_QUOTA_USAGE_ORIGIN, @@ -64,9 +60,7 @@ async function testClearData(clearSiteData, clearCache) { let doc = gBrowser.selectedBrowser.contentDocument; let clearSiteDataButton = doc.getElementById("clearSiteDataButton"); - let url = useOldClearHistoryDialog - ? "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml" - : "chrome://browser/content/sanitize_v2.xhtml"; + let url = "chrome://browser/content/preferences/dialogs/clearSiteData.xhtml"; let dialogOpened = promiseLoadSubDialog(url); clearSiteDataButton.doCommand(); let dialogWin = await dialogOpened; @@ -78,10 +72,8 @@ async function testClearData(clearSiteData, clearCache) { // since we've had cache intermittently changing under our feet. let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage); - let cookiesCheckboxId = useOldClearHistoryDialog - ? "clearSiteData" - : "cookiesAndStorage"; - let cacheCheckboxId = useOldClearHistoryDialog ? "clearCache" : "cache"; + let cookiesCheckboxId = "clearSiteData"; + let cacheCheckboxId = "clearCache"; let clearSiteDataCheckbox = dialogWin.document.getElementById(cookiesCheckboxId); let clearCacheCheckbox = dialogWin.document.getElementById(cacheCheckboxId); @@ -106,28 +98,13 @@ async function testClearData(clearSiteData, clearCache) { clearSiteDataCheckbox.checked = clearSiteData; clearCacheCheckbox.checked = clearCache; - if (!useOldClearHistoryDialog) { - // The new clear history dialog has a seperate checkbox for site settings - let siteSettingsCheckbox = - dialogWin.document.getElementById("siteSettings"); - siteSettingsCheckbox.checked = clearSiteData; - // select clear everything to match the old dialog boxes behaviour for this test - let timespanSelection = dialogWin.document.getElementById( - "sanitizeDurationChoice" - ); - timespanSelection.value = 0; - } // Some additional promises/assertions to wait for // when deleting site data. let acceptPromise; let updatePromise; let cookiesClearedPromise; if (clearSiteData) { - // the new clear history dialog does not have a extra prompt - // to clear site data after clicking clear - if (useOldClearHistoryDialog) { - acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); - } + acceptPromise = BrowserTestUtils.promiseAlertDialogOpen("accept"); updatePromise = promiseSiteDataManagerSitesUpdated(); cookiesClearedPromise = promiseCookiesCleared(); } @@ -137,7 +114,7 @@ async function testClearData(clearSiteData, clearCache) { let clearButton = dialogWin.document .querySelector("dialog") .getButton("accept"); - if (!clearSiteData && !clearCache && useOldClearHistoryDialog) { + if (!clearSiteData && !clearCache) { // Simulate user input on one of the checkboxes to trigger the event listener for // disabling the clearButton. clearCacheCheckbox.doCommand(); @@ -158,7 +135,7 @@ async function testClearData(clearSiteData, clearCache) { // For site data we display an extra warning dialog, make sure // to accept it. - if (clearSiteData && useOldClearHistoryDialog) { + if (clearSiteData) { await acceptPromise; } @@ -222,6 +199,12 @@ async function testClearData(clearSiteData, clearCache) { await SiteDataManager.removeAll(); } +add_setup(function () { + SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", true]], + }); +}); + // Test opening the "Clear All Data" dialog and cancelling. add_task(async function () { await testClearData(false, false); diff --git a/browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js b/browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js new file mode 100644 index 0000000000..8cb8be25b3 --- /dev/null +++ b/browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js @@ -0,0 +1,258 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +async function testClearData(clearSiteData, clearCache) { + PermissionTestUtils.add( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage", + Services.perms.ALLOW_ACTION + ); + + // Open a test site which saves into appcache. + await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_OFFLINE_URL); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Fill indexedDB with test data. + // Don't wait for the page to load, to register the content event handler as quickly as possible. + // If this test goes intermittent, we might have to tell the page to wait longer before + // firing the event. + BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_QUOTA_USAGE_URL, false); + await BrowserTestUtils.waitForContentEvent( + gBrowser.selectedBrowser, + "test-indexedDB-done", + false, + null, + true + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + // Register some service workers. + await loadServiceWorkerTestPage(TEST_SERVICE_WORKER_URL); + await promiseServiceWorkerRegisteredFor(TEST_SERVICE_WORKER_URL); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + // Test the initial states. + let cacheUsage = await SiteDataManager.getCacheSize(); + let quotaUsage = await SiteDataTestUtils.getQuotaUsage( + TEST_QUOTA_USAGE_ORIGIN + ); + let totalUsage = await SiteDataManager.getTotalUsage(); + Assert.greater(cacheUsage, 0, "The cache usage should not be 0"); + Assert.greater(quotaUsage, 0, "The quota usage should not be 0"); + Assert.greater(totalUsage, 0, "The total usage should not be 0"); + + let initialSizeLabelValue = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + async function () { + let sizeLabel = content.document.getElementById("totalSiteDataSize"); + return sizeLabel.textContent; + } + ); + + let doc = gBrowser.selectedBrowser.contentDocument; + let clearSiteDataButton = doc.getElementById("clearSiteDataButton"); + + let url = "chrome://browser/content/sanitize_v2.xhtml"; + let dialogOpened = promiseLoadSubDialog(url); + clearSiteDataButton.doCommand(); + let dialogWin = await dialogOpened; + + // Convert the usage numbers in the same way the UI does it to assert + // that they're displayed in the dialog. + let [convertedTotalUsage] = DownloadUtils.convertByteUnits(totalUsage); + // For cache we just assert that the right unit (KB, probably) is displayed, + // since we've had cache intermittently changing under our feet. + let [, convertedCacheUnit] = DownloadUtils.convertByteUnits(cacheUsage); + + let cookiesCheckboxId = "cookiesAndStorage"; + let cacheCheckboxId = "cache"; + let clearSiteDataCheckbox = + dialogWin.document.getElementById(cookiesCheckboxId); + let clearCacheCheckbox = dialogWin.document.getElementById(cacheCheckboxId); + // The usage details are filled asynchronously, so we assert that they're present by + // waiting for them to be filled in. + await Promise.all([ + TestUtils.waitForCondition( + () => + clearSiteDataCheckbox.label && + clearSiteDataCheckbox.label.includes(convertedTotalUsage), + "Should show the quota usage" + ), + TestUtils.waitForCondition( + () => + clearCacheCheckbox.label && + clearCacheCheckbox.label.includes(convertedCacheUnit), + "Should show the cache usage" + ), + ]); + + // Check the boxes according to our test input. + clearSiteDataCheckbox.checked = clearSiteData; + clearCacheCheckbox.checked = clearCache; + + // select clear everything to match the old dialog boxes behaviour for this test + let timespanSelection = dialogWin.document.getElementById( + "sanitizeDurationChoice" + ); + timespanSelection.value = 1; + + // Some additional promises/assertions to wait for + // when deleting site data. + let updatePromise; + if (clearSiteData) { + // the new clear history dialog does not have a extra prompt + // to clear site data after clicking clear + updatePromise = promiseSiteDataManagerSitesUpdated(); + } + + let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload"); + + let clearButton = dialogWin.document + .querySelector("dialog") + .getButton("accept"); + let cancelButton = dialogWin.document + .querySelector("dialog") + .getButton("cancel"); + + if (!clearSiteData && !clearCache) { + // Cancel, since we can't delete anything. + cancelButton.click(); + } else { + // Delete stuff! + clearButton.click(); + } + + await dialogClosed; + + if (clearCache) { + TestUtils.waitForCondition(async function () { + let usage = await SiteDataManager.getCacheSize(); + return usage == 0; + }, "The cache usage should be removed"); + } else { + Assert.greater( + await SiteDataManager.getCacheSize(), + 0, + "The cache usage should not be 0" + ); + } + + if (clearSiteData) { + await updatePromise; + await promiseServiceWorkersCleared(); + + TestUtils.waitForCondition(async function () { + let usage = await SiteDataManager.getTotalUsage(); + return usage == 0; + }, "The total usage should be removed"); + } else { + quotaUsage = await SiteDataTestUtils.getQuotaUsage(TEST_QUOTA_USAGE_ORIGIN); + totalUsage = await SiteDataManager.getTotalUsage(); + Assert.greater(quotaUsage, 0, "The quota usage should not be 0"); + Assert.greater(totalUsage, 0, "The total usage should not be 0"); + } + + if (clearCache || clearSiteData) { + // Check that the size label in about:preferences updates after we cleared data. + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ initialSizeLabelValue }], + async function (opts) { + let sizeLabel = content.document.getElementById("totalSiteDataSize"); + await ContentTaskUtils.waitForCondition( + () => sizeLabel.textContent != opts.initialSizeLabelValue, + "Site data size label should have updated." + ); + } + ); + } + + let permission = PermissionTestUtils.getPermissionObject( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage" + ); + is( + clearSiteData ? permission : permission.capability, + clearSiteData ? null : Services.perms.ALLOW_ACTION, + "Should have the correct permission state." + ); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SiteDataManager.removeAll(); +} + +add_setup(function () { + SpecialPowers.pushPrefEnv({ + set: [["privacy.sanitize.useOldClearHistoryDialog", false]], + }); + + // The tests in this file all test specific interactions with the new clear + // history dialog and can't be split up. + requestLongerTimeout(2); +}); + +// Test opening the "Clear All Data" dialog and cancelling. +add_task(async function testNoSiteDataNoCacheClearing() { + await testClearData(false, false); +}); + +// Test opening the "Clear All Data" dialog and removing all site data. +add_task(async function testSiteDataClearing() { + await testClearData(true, false); +}); + +// Test opening the "Clear All Data" dialog and removing all cache. +add_task(async function testCacheClearing() { + await testClearData(false, true); +}); + +// Test opening the "Clear All Data" dialog and removing everything. +add_task(async function testSiteDataAndCacheClearing() { + await testClearData(true, true); +}); + +// Test clearing persistent storage +add_task(async function testPersistentStorage() { + PermissionTestUtils.add( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage", + Services.perms.ALLOW_ACTION + ); + + await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true }); + + let doc = gBrowser.selectedBrowser.contentDocument; + let clearSiteDataButton = doc.getElementById("clearSiteDataButton"); + + let url = "chrome://browser/content/sanitize_v2.xhtml"; + let dialogOpened = promiseLoadSubDialog(url); + clearSiteDataButton.doCommand(); + let dialogWin = await dialogOpened; + let dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload"); + + let timespanSelection = dialogWin.document.getElementById( + "sanitizeDurationChoice" + ); + timespanSelection.value = 1; + let clearButton = dialogWin.document + .querySelector("dialog") + .getButton("accept"); + clearButton.click(); + await dialogClosed; + + let permission = PermissionTestUtils.getPermissionObject( + TEST_QUOTA_USAGE_ORIGIN, + "persistent-storage" + ); + is(permission, null, "Should have the correct permission state."); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); diff --git a/browser/components/preferences/translations.inc.xhtml b/browser/components/preferences/translations.inc.xhtml index 9463fde707..1143cbc6d0 100644 --- a/browser/components/preferences/translations.inc.xhtml +++ b/browser/components/preferences/translations.inc.xhtml @@ -19,45 +19,63 @@ <p id="translations-settings-description" data-l10n-id="translations-settings-description"/> - <div class="translations-settings-manage-list" - id="translations-settings-manage-always-translate-list"> + <moz-card class="translations-settings-manage-section" data-l10n-attrs="heading" + id="translations-settings-always-translate-section"> <div class="translations-settings-manage-language"> <h2 id="translations-settings-always-translate" data-l10n-id="translations-settings-always-translate"/> <xul:menulist id="translations-settings-always-translate-list" - data-l10n-id="translations-settings-add-language-button"> - <xul:menupopup/> + data-l10n-id="translations-settings-add-language-button" + aria-labelledby="translations-settings-always-translate"> + <!-- The list of <menuitem> will be dynamically inserted. --> + <xul:menupopup id="translations-settings-always-translate-popup"/> </xul:menulist> </div> - </div> + </moz-card> - <div id="translations-settings-manage-never-translate-list" - class="translations-settings-manage-list"> + <moz-card id="translations-settings-never-translate-section" + class="translations-settings-manage-section"> <div class="translations-settings-manage-language"> <h2 id="translations-settings-never-translate" data-l10n-id="translations-settings-never-translate"/> <xul:menulist id="translations-settings-never-translate-list" - data-l10n-id="translations-settings-add-language-button"> - <xul:menupopup/> + data-l10n-id="translations-settings-add-language-button" + aria-labelledby="translations-settings-never-translate"> + <!-- The list of <menuitem> will be dynamically inserted. --> + <xul:menupopup id="translations-settings-never-translate-popup"/> </xul:menulist> </div> - </div> + </moz-card> - <div id="translations-settings-never-sites-list" class="translations-settings-manage-list" > - <div class="translations-settings-manage-list-info" > + <moz-card id="translations-settings-never-sites-section" + class="translations-settings-manage-section"> + <div class="translations-settings-manage-section-info" > <h2 id="translations-settings-never-sites-header" data-l10n-id="translations-settings-never-sites-header"/> <p id="translations-settings-never-sites" data-l10n-id="translations-settings-never-sites-description"/> </div> - </div> + </moz-card> - <div id="translations-settings-manage-install-list" class="translations-settings-manage-list"> - <div class="translations-settings-manage-list-info"> - <h2 id="translations-settings-download-languages" - data-l10n-id="translations-settings-download-languages"/> + <moz-card id="translations-settings-download-section" + class="translations-settings-manage-section"> + <div class="translations-settings-manage-section-info"> + <h2 data-l10n-id="translations-settings-download-languages"/> <a is="moz-support-link" class="learnMore" id="download-languages-learn-more" data-l10n-id="translations-settings-download-languages-link" support-page="website-translation"/> </div> - </div> + <div class="translations-settings-languages-card"> + <h3 class="translations-settings-language-header" data-l10n-id="translations-settings-language-header"></h3> + <div class="translations-settings-language-list"> + <div class="translations-settings-language"> + <moz-button class="translations-settings-download-icon" type="ghost icon" + aria-label="translations-settings-download-all-languages"></moz-button> + <!-- The option to "All languages" is added here. + In translations.js the option to download individual languages is + added dynamically based on the supported language list --> + <label id="translations-settings-download-all-languages" data-l10n-id="translations-settings-download-all-languages"></label> + </div> + </div> + </div> + </moz-card> </div> diff --git a/browser/components/preferences/translations.js b/browser/components/preferences/translations.js index c9cfe472ac..1bfa021d59 100644 --- a/browser/components/preferences/translations.js +++ b/browser/components/preferences/translations.js @@ -4,6 +4,10 @@ /* import-globals-from preferences.js */ +ChromeUtils.defineESModuleGetters(this, { + TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", +}); + let gTranslationsPane = { init() { document @@ -11,5 +15,271 @@ let gTranslationsPane = { .addEventListener("click", function () { gotoPref("general"); }); + + document + .getElementById("translations-settings-always-translate-list") + .addEventListener("command", this.addAlwaysLanguage); + + document + .getElementById("translations-settings-never-translate-list") + .addEventListener("command", this.addNeverLanguage); + + this.buildLanguageDropDowns(); + this.buildDownloadLanguageList(); + }, + + /** + * Populate the Drop down list with the list of supported languages + * for the user to choose languages to add to Always translate and + * Never translate settings list. + */ + async buildLanguageDropDowns() { + const { fromLanguages } = await TranslationsParent.getSupportedLanguages(); + let alwaysLangPopup = document.getElementById( + "translations-settings-always-translate-popup" + ); + let neverLangPopup = document.getElementById( + "translations-settings-never-translate-popup" + ); + + for (const { langTag, displayName } of fromLanguages) { + const alwaysLang = document.createXULElement("menuitem"); + alwaysLang.setAttribute("value", langTag); + alwaysLang.setAttribute("label", displayName); + alwaysLangPopup.appendChild(alwaysLang); + const neverLang = document.createXULElement("menuitem"); + neverLang.setAttribute("value", langTag); + neverLang.setAttribute("label", displayName); + neverLangPopup.appendChild(neverLang); + } + }, + + /** + * Show a list of languages for the user to be able to install + * and uninstall language models for local translation. + */ + async buildDownloadLanguageList() { + const supportedLanguages = await TranslationsParent.getSupportedLanguages(); + const languageList = TranslationsParent.getLanguageList(supportedLanguages); + + let installList = document.querySelector( + ".translations-settings-language-list" + ); + + // The option to download "All languages" is added in xhtml. + // Here the option to download individual languages is dynamically added + // based on the supported language list + installList + .querySelector("moz-button") + .addEventListener("click", installLanguage); + + for (const language of languageList) { + const languageElement = document.createElement("div"); + languageElement.classList.add("translations-settings-language"); + + const languageLabel = document.createElement("label"); + languageLabel.textContent = language.displayName; + languageLabel.setAttribute("value", language.langTag); + // Using the language tag suffix to create unique id for each language + languageLabel.id = "translations-settings-download-" + language.langTag; + + const installButton = document.createElement("moz-button"); + installButton.classList.add("translations-settings-download-icon"); + installButton.setAttribute("type", "ghost icon"); + installButton.addEventListener("click", installLanguage); + installButton.setAttribute("aria-label", languageLabel.id); + + languageElement.appendChild(installButton); + languageElement.appendChild(languageLabel); + installList.appendChild(languageElement); + } + }, + + /** + * Event handler when the user wants to add a language to + * Always translate settings list. + */ + addAlwaysLanguage(event) { + /* TODO: + The function addLanguage adds the HTML element. + It will be moved to the observer in the next Bug - 1881259 . + It is here just to test the UI. + For now a language can be added multiple times. + + In the next bug we will maintain a local state of preferences. + When a language is added or removed, the user event updates the preferences in the Services, + This triggers the observer which compares the preferences in the local state and the + preferences in the Services and adds or removes the language in the local state and updates the + UI to reflect the updated Preferences in the Services. + */ + addLanguage( + event, + "translations-settings-always-translate-section", + deleteAlwaysLanguage + ); + }, + + /** + * Event handler when the user wants to add a language to + * Never translate settings list. + */ + addNeverLanguage(event) { + /* TODO: + The function addLanguage adds the HTML element. + It will be moved to the observer in the next Bug - 1881259 . + It is here just to test the UI. + For now a language can be added multiple times. + + In the next bug we will maintain a local state of preferences. + When a language is added or removed, the user event updates the preferences in the Services, + This triggers the observer which compares the preferences in the local state and the + preferences in the Services and adds or removes the language in the local state and updates the + UI to reflect the updated Preferences in the Services. + */ + addLanguage( + event, + "translations-settings-never-translate-section", + deleteNeverLanguage + ); }, }; + +/** + * Function to add a language selected by the user to the list of + * Always/Never translate settings list. + */ +async function addLanguage(event, listClass, delHandler) { + const translatePrefix = + listClass === "translations-settings-never-translate-section" + ? "never" + : "always"; + let translateSection = document.getElementById(listClass); + let languageList = translateSection.querySelector( + ".translations-settings-language-list" + ); + + // While adding the first language, add the Header and language List div + if (!languageList) { + let languageCard = document.createElement("div"); + languageCard.classList.add("translations-settings-languages-card"); + translateSection.appendChild(languageCard); + + let languageHeader = document.createElement("h3"); + languageCard.appendChild(languageHeader); + languageHeader.setAttribute( + "data-l10n-id", + "translations-settings-language-header" + ); + languageHeader.classList.add("translations-settings-language-header"); + + languageList = document.createElement("div"); + languageList.classList.add("translations-settings-language-list"); + languageCard.appendChild(languageList); + } + const languageElement = document.createElement("div"); + languageElement.classList.add("translations-settings-language"); + // Add the language after the Language Header + languageList.insertBefore(languageElement, languageList.firstChild); + + const languageLabel = document.createElement("label"); + languageLabel.textContent = event.target.getAttribute("label"); + languageLabel.setAttribute("value", event.target.getAttribute("value")); + // Using the language tag suffix to create unique id for each language + // add prefix for the always/never translate + languageLabel.id = + "translations-settings-language-" + + translatePrefix + + "-" + + event.target.getAttribute("value"); + + const delButton = document.createElement("moz-button"); + delButton.classList.add("translations-settings-delete-icon"); + delButton.setAttribute("type", "ghost icon"); + delButton.addEventListener("click", delHandler); + delButton.setAttribute("aria-label", languageLabel.id); + + languageElement.appendChild(delButton); + languageElement.appendChild(languageLabel); + + /* After a language is selected the menulist button display will be set to the + selected langauge. After processing the button event the + data-l10n-id of the menulist button is restored to "Add Language" */ + const menuList = translateSection.querySelector("menulist"); + await document.l10n.translateElements([menuList]); +} + +/** + * Event Handler to delete a language selected by the user from the list of + * Always translate settings list. + */ +function deleteAlwaysLanguage(event) { + /* TODO: + The function removeLanguage removes the HTML element. + It will be moved to the observer in the next Bug - 1881259 . + It is here just to test the UI. + + In the next bug we will maintain a local state of preferences. + When a language is added or removed, the user event updates the preferences in the Services, + This triggers the observer which compares the preferences in the local state and the + preferences in the Services and adds or removes the language in the local state and updates the + UI to reflect the updated Preferences in the Services. + */ + removeLanguage(event); +} + +/** + * Event Handler to delete a language selected by the user from the list of + * Never translate settings list. + */ +function deleteNeverLanguage(event) { + /* TODO: + The function removeLanguage removes the HTML element. + It will be moved to the observer in the next Bug - 1881259 . + It is here just to test the UI. + + In the next bug we will maintain a local state of preferences. + When a language is added or removed, the user event updates the preferences in the Services, + This triggers the observer which compares the preferences in the local state and the + preferences in the Services and adds or removes the language in the local state and updates the + UI to reflect the updated Preferences in the Services. + */ + removeLanguage(event); +} + +/** + * Function to delete a language selected by the user from the list of + * Always/Never translate settings list. + */ +function removeLanguage(event) { + /* Langauge section moz-card -parent of-> Language card -parent of-> + Language heading and Language list -parent of-> + Language Element -parent of-> language button and label + */ + let languageCard = event.target.parentNode.parentNode.parentNode; + event.target.parentNode.remove(); + if (languageCard.children[1].childElementCount === 0) { + // If there is no language in the list remove the + // Language Header and language list div + languageCard.remove(); + } +} + +/** + * Event Handler to install a language model selected by the user + */ +function installLanguage(event) { + event.target.classList.remove("translations-settings-download-icon"); + event.target.classList.add("translations-settings-delete-icon"); + event.target.removeEventListener("click", installLanguage); + event.target.addEventListener("click", unInstallLanguage); +} + +/** + * Event Handler to install a language model selected by the user + */ +function unInstallLanguage(event) { + event.target.classList.remove("translations-settings-delete-icon"); + event.target.classList.add("translations-settings-download-icon"); + event.target.removeEventListener("click", unInstallLanguage); + event.target.addEventListener("click", installLanguage); +} diff --git a/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js b/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js index a1b9420171..818c412ef6 100644 --- a/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js +++ b/browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js @@ -9,7 +9,7 @@ const DUMMY_PAGE = PATH + "empty_file.html"; add_task( async function test_principal_right_click_open_link_in_new_private_win() { - await BrowserTestUtils.withNewTab(TEST_PAGE, async function (browser) { + await BrowserTestUtils.withNewTab(TEST_PAGE, async function () { let promiseNewWindow = BrowserTestUtils.waitForNewWindow({ url: DUMMY_PAGE, }); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js index bfe5708a5b..ef207916a5 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js @@ -41,7 +41,7 @@ add_task(async function test_experiment_messaging_system_dismiss() { let { win: win1, tab: tab1 } = await openTabAndWaitForRender(); - await SpecialPowers.spawn(tab1, [LOCALE], async function (locale) { + await SpecialPowers.spawn(tab1, [LOCALE], async function () { content.document.querySelector("#dismiss-btn").click(); info("button clicked"); }); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js index ac42caa2dd..0d4b0a1dbb 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js @@ -47,7 +47,7 @@ add_task(async function test_experiment_messaging_system_impressions() { let { win: win1, tab: tab1 } = await openTabAndWaitForRender(); - await SpecialPowers.spawn(tab1, [LOCALE], async function (locale) { + await SpecialPowers.spawn(tab1, [LOCALE], async function () { is( content.document .querySelector(".promo button") @@ -72,7 +72,7 @@ add_task(async function test_experiment_messaging_system_impressions() { let { win: win2, tab: tab2 } = await openTabAndWaitForRender(); - await SpecialPowers.spawn(tab2, [LOCALE], async function (locale) { + await SpecialPowers.spawn(tab2, [LOCALE], async function () { is( content.document .querySelector(".promo button") diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js index de6aa1f6ba..9d274a28de 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js @@ -46,7 +46,7 @@ function getStorageEntryCount(device, goon) { var visitor = { entryCount: 0, - onCacheStorageInfo(aEntryCount, aConsumption) {}, + onCacheStorageInfo() {}, onCacheEntryInfo(uri) { var urispec = uri.asciiSpec; info(device + ":" + urispec + "\n"); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js index 9b796613a9..acdb4bb30d 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js @@ -31,7 +31,7 @@ function test() { }; function testCheckbox() { win.removeEventListener("load", testCheckbox); - Services.obs.addObserver(function onCertUI(aSubject, aTopic, aData) { + Services.obs.addObserver(function onCertUI() { Services.obs.removeObserver(onCertUI, "cert-exception-ui-ready"); ok(win.gCert, "The certificate information should be available now"); diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js index 39e41589b4..ce86cab69c 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js @@ -19,7 +19,7 @@ add_task(async () => { await BrowserTestUtils.browserLoaded(privateTab); let observerExited = { - observe(aSubject, aTopic, aData) { + observe() { ok(false, "Notification received!"); }, }; diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js index eea0ab07ca..46ef974677 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js @@ -32,7 +32,7 @@ function clearAllPlacesFavicons() { return new Promise(resolve => { let observer = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if (aTopic === "places-favicons-expired") { resolve(); Services.obs.removeObserver(observer, "places-favicons-expired"); @@ -59,7 +59,7 @@ function observeFavicon(aIsPrivate, aExpectedCookie, aPageURI) { return new Promise(resolve => { let observer = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { // Make sure that the topic is 'http-on-modify-request'. if (aTopic === "http-on-modify-request") { // We check the privateBrowsingId for the originAttributes of the loading @@ -121,7 +121,7 @@ function observeFavicon(aIsPrivate, aExpectedCookie, aPageURI) { function waitOnFaviconResponse(aFaviconURL) { return new Promise(resolve => { let observer = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { if ( aTopic === "http-on-examine-response" || aTopic === "http-on-examine-cached-response" diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html index 01ed3f3d2c..e7c1920215 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html @@ -5,7 +5,7 @@ </head> <body> <script type="text/javascript"> - navigator.geolocation.getCurrentPosition(function(pos) { + navigator.geolocation.getCurrentPosition(function() { // ignore }); </script> diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js index 1fd28d4ca6..4874e61bd4 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js @@ -6,7 +6,7 @@ add_task(async function test_no_notification_when_pb_autostart() { let observedLastPBContext = false; let observerExited = { - observe(aSubject, aTopic, aData) { + observe() { observedLastPBContext = true; }, }; @@ -31,7 +31,7 @@ add_task(async function test_no_notification_when_pb_autostart() { add_task(async function test_notification_when_about_preferences() { let observedLastPBContext = false; let observerExited = { - observe(aSubject, aTopic, aData) { + observe() { observedLastPBContext = true; }, }; diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js index c46417933a..c57a482752 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js @@ -11,7 +11,7 @@ function test() { let expectedExiting = true; let expectedExited = false; let observerExiting = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { is( aTopic, "last-pb-context-exiting", @@ -26,7 +26,7 @@ function test() { }, }; let observerExited = { - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { is( aTopic, "last-pb-context-exited", diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js index ab74caeb5e..70f6666589 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js @@ -55,7 +55,7 @@ function test() { } function openPrivateBrowsingModeByUI(aWindow, aCallback) { - Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.addObserver(function observer(aSubject) { aSubject.addEventListener( "load", function () { diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js index dd0e2e1b64..ac31b925ed 100644 --- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js @@ -12,7 +12,7 @@ add_task(async function test() { function promiseLocationChange() { return new Promise(resolve => { - Services.obs.addObserver(function onLocationChange(subj, topic, data) { + Services.obs.addObserver(function onLocationChange(subj, topic) { Services.obs.removeObserver(onLocationChange, topic); resolve(); }, "browser-fullZoom:location-change"); @@ -59,7 +59,7 @@ add_task(async function test() { ); } - function testOnWindow(options, callback) { + function testOnWindow(options) { return BrowserTestUtils.openNewBrowserWindow(options).then(win => { windowsToClose.push(win); windowsToReset.push(win); diff --git a/browser/components/protections/content/protections.mjs b/browser/components/protections/content/protections.mjs index 3204586a2b..412ac54d1b 100644 --- a/browser/components/protections/content/protections.mjs +++ b/browser/components/protections/content/protections.mjs @@ -31,7 +31,7 @@ if (searchParams.has("entrypoint")) { searchParamsChanged = true; } -document.addEventListener("DOMContentLoaded", e => { +document.addEventListener("DOMContentLoaded", () => { if (searchParamsChanged) { let newURL = protocol + pathname; let params = searchParams.toString(); diff --git a/browser/components/protections/test/browser/browser_protections_monitor.js b/browser/components/protections/test/browser/browser_protections_monitor.js index b24d8de55c..e96412edca 100644 --- a/browser/components/protections/test/browser/browser_protections_monitor.js +++ b/browser/components/protections/test/browser/browser_protections_monitor.js @@ -134,7 +134,7 @@ add_task(async function () { await BrowserTestUtils.removeTab(tab); }); -async function checkNoLoginsContentIsDisplayed(tab, expectedLinkContent) { +async function checkNoLoginsContentIsDisplayed(tab) { await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { await ContentTaskUtils.waitForCondition(() => { const noLogins = content.document.querySelector( diff --git a/browser/components/protocolhandler/WebProtocolHandlerRegistrar.sys.mjs b/browser/components/protocolhandler/WebProtocolHandlerRegistrar.sys.mjs index 345046ae27..5255bfec46 100644 --- a/browser/components/protocolhandler/WebProtocolHandlerRegistrar.sys.mjs +++ b/browser/components/protocolhandler/WebProtocolHandlerRegistrar.sys.mjs @@ -543,7 +543,7 @@ WebProtocolHandlerRegistrar.prototype = { notificationId, { label: { - "l10n-id": "protocolhandler-mailto-handler-set-message", + "l10n-id": "protocolhandler-mailto-handler-set", "l10n-args": { url: aURI.host }, }, priority: osDefaultNotificationBox.PRIORITY_INFO_LOW, @@ -576,7 +576,7 @@ WebProtocolHandlerRegistrar.prototype = { true ); newitem.messageL10nId = - "protocolhandler-mailto-handler-confirm-message"; + "protocolhandler-mailto-handler-confirm"; newitem.removeChild(newitem.buttonContainer); newitem.setAttribute("type", "success"); // from moz-message-bar.css newitem.eventCallback = null; // disable show only once per day for success diff --git a/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs b/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs index ef1f4e1270..836908c7b4 100644 --- a/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs +++ b/browser/components/reportbrokensite/ReportBrokenSite.sys.mjs @@ -594,7 +594,7 @@ export var ReportBrokenSite = new (class ReportBrokenSite { const expectedBrowser = tabbrowser.getBrowserForTab(tab); return new Promise(resolve => { const listener = { - onLocationChange(browser, webProgress, request, uri, flags) { + onLocationChange(browser, webProgress, request, uri) { if ( browser == expectedBrowser && uri.spec == url && diff --git a/browser/components/reportbrokensite/test/browser/browser_back_buttons.js b/browser/components/reportbrokensite/test/browser/browser_back_buttons.js index b8de5f8e95..c004442c24 100644 --- a/browser/components/reportbrokensite/test/browser/browser_back_buttons.js +++ b/browser/components/reportbrokensite/test/browser/browser_back_buttons.js @@ -12,26 +12,23 @@ add_common_setup(); add_task(async function testBackButtonsAreAdded() { ensureReportBrokenSitePreffedOn(); - await BrowserTestUtils.withNewTab( - REPORTABLE_PAGE_URL, - async function (browser) { - let rbs = await AppMenu().openReportBrokenSite(); - rbs.isBackButtonEnabled(); - await rbs.clickBack(); - await rbs.close(); - - rbs = await HelpMenu().openReportBrokenSite(); - ok(!rbs.backButton, "Back button is not shown for Help Menu"); - await rbs.close(); - - rbs = await ProtectionsPanel().openReportBrokenSite(); - rbs.isBackButtonEnabled(); - await rbs.clickBack(); - await rbs.close(); - - rbs = await HelpMenu().openReportBrokenSite(); - ok(!rbs.backButton, "Back button is not shown for Help Menu"); - await rbs.close(); - } - ); + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + let rbs = await AppMenu().openReportBrokenSite(); + rbs.isBackButtonEnabled(); + await rbs.clickBack(); + await rbs.close(); + + rbs = await HelpMenu().openReportBrokenSite(); + ok(!rbs.backButton, "Back button is not shown for Help Menu"); + await rbs.close(); + + rbs = await ProtectionsPanel().openReportBrokenSite(); + rbs.isBackButtonEnabled(); + await rbs.clickBack(); + await rbs.close(); + + rbs = await HelpMenu().openReportBrokenSite(); + ok(!rbs.backButton, "Back button is not shown for Help Menu"); + await rbs.close(); + }); }); diff --git a/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js b/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js index 4c37866628..3bf9278e46 100644 --- a/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js +++ b/browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js @@ -12,34 +12,31 @@ add_common_setup(); requestLongerTimeout(2); async function testPressingKey(key, tabToMatch, makePromise, followUp) { - await BrowserTestUtils.withNewTab( - REPORTABLE_PAGE_URL, - async function (browser) { - for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) { - info( - `Opening RBS to test pressing ${key} for ${tabToMatch} on ${menu.menuDescription}` - ); - const rbs = await menu.openReportBrokenSite(); - const promise = makePromise(rbs); - if (tabToMatch) { - if (await tabTo(tabToMatch)) { - await pressKeyAndAwait(promise, key); - followUp && (await followUp(rbs)); - await rbs.close(); - ok(true, `was able to activate ${tabToMatch} with keyboard`); - } else { - await rbs.close(); - ok(false, `could not tab to ${tabToMatch}`); - } - } else { + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + for (const menu of [AppMenu(), ProtectionsPanel(), HelpMenu()]) { + info( + `Opening RBS to test pressing ${key} for ${tabToMatch} on ${menu.menuDescription}` + ); + const rbs = await menu.openReportBrokenSite(); + const promise = makePromise(rbs); + if (tabToMatch) { + if (await tabTo(tabToMatch)) { await pressKeyAndAwait(promise, key); followUp && (await followUp(rbs)); await rbs.close(); - ok(true, `was able to use keyboard`); + ok(true, `was able to activate ${tabToMatch} with keyboard`); + } else { + await rbs.close(); + ok(false, `could not tab to ${tabToMatch}`); } + } else { + await pressKeyAndAwait(promise, key); + followUp && (await followUp(rbs)); + await rbs.close(); + ok(true, `was able to use keyboard`); } } - ); + }); } add_task(async function testSendMoreInfo() { @@ -98,16 +95,13 @@ add_task(async function testESCOnSent() { add_task(async function testBackButtons() { ensureReportBrokenSitePreffedOn(); - await BrowserTestUtils.withNewTab( - REPORTABLE_PAGE_URL, - async function (browser) { - for (const menu of [AppMenu(), ProtectionsPanel()]) { - await menu.openReportBrokenSite(); - await tabTo("#report-broken-site-popup-mainView .subviewbutton-back"); - const promise = BrowserTestUtils.waitForEvent(menu.popup, "ViewShown"); - await pressKeyAndAwait(promise, "KEY_Enter"); - menu.close(); - } + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + for (const menu of [AppMenu(), ProtectionsPanel()]) { + await menu.openReportBrokenSite(); + await tabTo("#report-broken-site-popup-mainView .subviewbutton-back"); + const promise = BrowserTestUtils.waitForEvent(menu.popup, "ViewShown"); + await pressKeyAndAwait(promise, "KEY_Enter"); + menu.close(); } - ); + }); }); diff --git a/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js b/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js index 7097a662e5..68aeae911e 100644 --- a/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js +++ b/browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js @@ -33,7 +33,7 @@ add_task(async function testReportSentViewBGColor() { await SpecialPowers.pushPrefEnv({ set: HIGH_CONTRAST_MODE_OFF }); const rbs = await menu.openReportBrokenSite(); const { mainView, sentView } = rbs; - mainView.style.backgroundColor = "var(--color-background-success)"; + mainView.style.backgroundColor = "var(--background-color-success)"; const expectedReportSentBGColor = defaultView.getComputedStyle(mainView).backgroundColor; mainView.style.backgroundColor = ""; diff --git a/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js b/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js index 0f5545fcc4..e6e6967919 100644 --- a/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js +++ b/browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js @@ -29,45 +29,42 @@ async function clickSendAndCheckPing(rbs, expectedReason = null) { add_task(async function testReasonDropdown() { ensureReportBrokenSitePreffedOn(); - await BrowserTestUtils.withNewTab( - REPORTABLE_PAGE_URL, - async function (browser) { - ensureReasonDisabled(); - - let rbs = await AppMenu().openReportBrokenSite(); - await rbs.isReasonHidden(); - await rbs.isSendButtonEnabled(); - await clickSendAndCheckPing(rbs); - await rbs.clickOkay(); - - ensureReasonOptional(); - rbs = await AppMenu().openReportBrokenSite(); - await rbs.isReasonOptional(); - await rbs.isSendButtonEnabled(); - await clickSendAndCheckPing(rbs); - await rbs.clickOkay(); - - rbs = await AppMenu().openReportBrokenSite(); - await rbs.isReasonOptional(); - rbs.chooseReason("slow"); - await rbs.isSendButtonEnabled(); - await clickSendAndCheckPing(rbs, "slow"); - await rbs.clickOkay(); - - ensureReasonRequired(); - rbs = await AppMenu().openReportBrokenSite(); - await rbs.isReasonRequired(); - await rbs.isSendButtonEnabled(); - const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window); - EventUtils.synthesizeMouseAtCenter(rbs.sendButton, {}, window); - await selectPromise; - rbs.chooseReason("media"); - await rbs.dismissDropdownPopup(); - await rbs.isSendButtonEnabled(); - await clickSendAndCheckPing(rbs, "media"); - await rbs.clickOkay(); - } - ); + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + ensureReasonDisabled(); + + let rbs = await AppMenu().openReportBrokenSite(); + await rbs.isReasonHidden(); + await rbs.isSendButtonEnabled(); + await clickSendAndCheckPing(rbs); + await rbs.clickOkay(); + + ensureReasonOptional(); + rbs = await AppMenu().openReportBrokenSite(); + await rbs.isReasonOptional(); + await rbs.isSendButtonEnabled(); + await clickSendAndCheckPing(rbs); + await rbs.clickOkay(); + + rbs = await AppMenu().openReportBrokenSite(); + await rbs.isReasonOptional(); + rbs.chooseReason("slow"); + await rbs.isSendButtonEnabled(); + await clickSendAndCheckPing(rbs, "slow"); + await rbs.clickOkay(); + + ensureReasonRequired(); + rbs = await AppMenu().openReportBrokenSite(); + await rbs.isReasonRequired(); + await rbs.isSendButtonEnabled(); + const selectPromise = BrowserTestUtils.waitForSelectPopupShown(window); + EventUtils.synthesizeMouseAtCenter(rbs.sendButton, {}, window); + await selectPromise; + rbs.chooseReason("media"); + await rbs.dismissDropdownPopup(); + await rbs.isSendButtonEnabled(); + await clickSendAndCheckPing(rbs, "media"); + await rbs.clickOkay(); + }); }); async function getListItems(rbs) { @@ -90,72 +87,69 @@ add_task(async function testReasonDropdownRandomized() { undefined ); - await BrowserTestUtils.withNewTab( - REPORTABLE_PAGE_URL, - async function (browser) { - // confirm that the default order is initially used - Services.prefs.setBoolPref(RANDOMIZE_PREF, false); - const rbs = await AppMenu().openReportBrokenSite(); - const defaultOrder = [ - "choose", - "slow", - "media", - "content", - "account", - "adblockers", - "other", - ]; - Assert.deepEqual( - await getListItems(rbs), - defaultOrder, - "non-random order is correct" - ); - - // confirm that a random order happens per user - let randomOrder; - let isRandomized = false; - Services.prefs.setBoolPref(RANDOMIZE_PREF, true); - - // This becomes ClientEnvironment.randomizationId, which we can set to - // any value which results in a different order from the default ordering. - Services.prefs.setCharPref("app.normandy.user_id", "dummy"); - - // clicking cancel triggers a reset, which is when the randomization - // logic is called. so we must click cancel after pref-changes here. + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + // confirm that the default order is initially used + Services.prefs.setBoolPref(RANDOMIZE_PREF, false); + const rbs = await AppMenu().openReportBrokenSite(); + const defaultOrder = [ + "choose", + "slow", + "media", + "content", + "account", + "adblockers", + "other", + ]; + Assert.deepEqual( + await getListItems(rbs), + defaultOrder, + "non-random order is correct" + ); + + // confirm that a random order happens per user + let randomOrder; + let isRandomized = false; + Services.prefs.setBoolPref(RANDOMIZE_PREF, true); + + // This becomes ClientEnvironment.randomizationId, which we can set to + // any value which results in a different order from the default ordering. + Services.prefs.setCharPref("app.normandy.user_id", "dummy"); + + // clicking cancel triggers a reset, which is when the randomization + // logic is called. so we must click cancel after pref-changes here. + rbs.clickCancel(); + await AppMenu().openReportBrokenSite(); + randomOrder = await getListItems(rbs); + Assert.ok( + randomOrder != defaultOrder, + "options are randomized with pref on" + ); + + // confirm that the order doesn't change per user + isRandomized = false; + for (let attempt = 0; attempt < 5; ++attempt) { rbs.clickCancel(); await AppMenu().openReportBrokenSite(); - randomOrder = await getListItems(rbs); - Assert.ok( - randomOrder != defaultOrder, - "options are randomized with pref on" - ); + const order = await getListItems(rbs); - // confirm that the order doesn't change per user - isRandomized = false; - for (let attempt = 0; attempt < 5; ++attempt) { - rbs.clickCancel(); - await AppMenu().openReportBrokenSite(); - const order = await getListItems(rbs); - - if (order != randomOrder) { - isRandomized = true; - break; - } + if (order != randomOrder) { + isRandomized = true; + break; } - Assert.ok(!isRandomized, "options keep the same order per user"); - - // confirm that the order reverts to the default if pref flipped to false - Services.prefs.setBoolPref(RANDOMIZE_PREF, false); - rbs.clickCancel(); - await AppMenu().openReportBrokenSite(); - Assert.deepEqual( - defaultOrder, - await getListItems(rbs), - "reverts to non-random order correctly" - ); - rbs.clickCancel(); } - ); + Assert.ok(!isRandomized, "options keep the same order per user"); + + // confirm that the order reverts to the default if pref flipped to false + Services.prefs.setBoolPref(RANDOMIZE_PREF, false); + rbs.clickCancel(); + await AppMenu().openReportBrokenSite(); + Assert.deepEqual( + defaultOrder, + await getListItems(rbs), + "reverts to non-random order correctly" + ); + rbs.clickCancel(); + }); Services.prefs.setCharPref(USER_ID_PREF, origNormandyUserID); }); diff --git a/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js b/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js index 26101d77b9..98c6c740a5 100644 --- a/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js +++ b/browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js @@ -40,14 +40,11 @@ async function testEnabledForValidURLs(menu) { ensureReportBrokenSitePreffedOff(); ensureReportSiteIssuePreffedOn(); - await BrowserTestUtils.withNewTab( - REPORTABLE_PAGE_URL, - async function (browser) { - await menu.open(); - menu.isReportSiteIssueEnabled(); - await menu.close(); - } - ); + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + await menu.open(); + menu.isReportSiteIssueEnabled(); + await menu.close(); + }); } // AppMenu help sub-menu diff --git a/browser/components/reportbrokensite/test/browser/browser_send_more_info.js b/browser/components/reportbrokensite/test/browser/browser_send_more_info.js index edce03e0e0..9306f5161e 100644 --- a/browser/components/reportbrokensite/test/browser/browser_send_more_info.js +++ b/browser/components/reportbrokensite/test/browser/browser_send_more_info.js @@ -24,22 +24,19 @@ requestLongerTimeout(2); add_task(async function testSendMoreInfoPref() { ensureReportBrokenSitePreffedOn(); - await BrowserTestUtils.withNewTab( - REPORTABLE_PAGE_URL, - async function (browser) { - await changeTab(gBrowser.selectedTab, REPORTABLE_PAGE_URL); - - ensureSendMoreInfoDisabled(); - let rbs = await AppMenu().openReportBrokenSite(); - await rbs.isSendMoreInfoHidden(); - await rbs.close(); - - ensureSendMoreInfoEnabled(); - rbs = await AppMenu().openReportBrokenSite(); - await rbs.isSendMoreInfoShown(); - await rbs.close(); - } - ); + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + await changeTab(gBrowser.selectedTab, REPORTABLE_PAGE_URL); + + ensureSendMoreInfoDisabled(); + let rbs = await AppMenu().openReportBrokenSite(); + await rbs.isSendMoreInfoHidden(); + await rbs.close(); + + ensureSendMoreInfoEnabled(); + rbs = await AppMenu().openReportBrokenSite(); + await rbs.isSendMoreInfoShown(); + await rbs.close(); + }); }); add_task(async function testSendingMoreInfo() { diff --git a/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js b/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js index 3a50c9aa51..e02c6a8394 100644 --- a/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js +++ b/browser/components/reportbrokensite/test/browser/browser_tab_key_order.js @@ -124,12 +124,9 @@ add_task(async function testTabOrdering() { ensureReportBrokenSitePreffedOn(); ensureSendMoreInfoEnabled(); - await BrowserTestUtils.withNewTab( - REPORTABLE_PAGE_URL, - async function (browser) { - await testTabOrder(AppMenu()); - await testTabOrder(ProtectionsPanel()); - await testTabOrder(HelpMenu()); - } - ); + await BrowserTestUtils.withNewTab(REPORTABLE_PAGE_URL, async function () { + await testTabOrder(AppMenu()); + await testTabOrder(ProtectionsPanel()); + await testTabOrder(HelpMenu()); + }); }); diff --git a/browser/components/reportbrokensite/test/browser/head.js b/browser/components/reportbrokensite/test/browser/head.js index 7cc1d51a21..84aa0f56dc 100644 --- a/browser/components/reportbrokensite/test/browser/head.js +++ b/browser/components/reportbrokensite/test/browser/head.js @@ -833,7 +833,7 @@ async function tabTo(match, win = window) { return undefined; } -async function setupStrictETP(fn) { +async function setupStrictETP() { await UrlClassifierTestUtils.addTestTrackers(); registerCleanupFunction(() => { UrlClassifierTestUtils.cleanupTestTrackers(); diff --git a/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js b/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js index 13dcec8ea1..69443db930 100644 --- a/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js +++ b/browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js @@ -160,7 +160,7 @@ async function test_dynamical_window_rounding(aWindow, aCheckFunc) { * check() functions use ok() while on Linux, we do not all ok() and instead * rely on waitForCondition to fail). * - * The logging statements in this test, and RFPHelper.jsm, help narrow down and + * The logging statements in this test, and RFPHelper.sys.mjs, help narrow down and * illustrate the issue. */ info(caseString + "We hit the weird resize bug. Resize it again."); diff --git a/browser/components/resistfingerprinting/test/browser/browser_navigator.js b/browser/components/resistfingerprinting/test/browser/browser_navigator.js index fb2c539194..2e7e76fdfb 100644 --- a/browser/components/resistfingerprinting/test/browser/browser_navigator.js +++ b/browser/components/resistfingerprinting/test/browser/browser_navigator.js @@ -339,7 +339,7 @@ async function testWorkerNavigator() { // test in Fission. if (SpecialPowers.useRemoteSubframes) { await new Promise(resolve => { - let observer = (subject, topic, data) => { + let observer = (subject, topic) => { if (topic === "ipc:content-shutdown") { Services.obs.removeObserver(observer, "ipc:content-shutdown"); resolve(); diff --git a/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js b/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js index a8f9db9edc..c070c7485b 100644 --- a/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js +++ b/browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js @@ -2081,7 +2081,7 @@ async function testKeyEvent(aTab, aTestCase) { // a custom event 'resultAvailable' for informing the script to check the // result. await new Promise(resolve => { - function eventHandler(aEvent) { + function eventHandler() { verifyKeyboardEvent( JSON.parse(resElement.value), result, diff --git a/browser/components/resistfingerprinting/test/browser/browser_timezone.js b/browser/components/resistfingerprinting/test/browser/browser_timezone.js index d2aefff01c..13deeb5b26 100644 --- a/browser/components/resistfingerprinting/test/browser/browser_timezone.js +++ b/browser/components/resistfingerprinting/test/browser/browser_timezone.js @@ -55,6 +55,15 @@ async function verifySpoofed() { "The hours reports in UTC timezone." ); is(date.getTimezoneOffset(), 0, "The difference with UTC timezone is 0."); + + let parser = new DOMParser(); + let doc = parser.parseFromString("<p></p>", "text/html"); + let lastModified = new Date( + doc.lastModified.replace(/(\d{2})\/(\d{2})\/(\d{4})/, "$3-$1-$2") + ); + // Use ceil to account for the time passed to run the other statements + let offset = Math.ceil((lastModified - new Date()) / 1000); + is(offset, 0, "document.lastModified does not leak the timezone."); } // Run test in the context of the page. diff --git a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html index da86656bd4..758176691b 100644 --- a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html +++ b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html @@ -2,7 +2,7 @@ <head> <meta charset="utf8"> <script> -function waitForCondition(aCond, aCallback, aErrorMsg) { +function waitForCondition(aCond, aCallback) { var tries = 0; var interval = setInterval(() => { if (tries >= 30) { diff --git a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html index 234661a6a9..c32bd40610 100644 --- a/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html +++ b/browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html @@ -5,7 +5,7 @@ <title></title> <script src="shared_test_funcs.js"></script> <script> -async function runTheTest(iframe_domain, cross_origin_domain, extraData) { +async function runTheTest(iframe_domain, cross_origin_domain) { const iframes = document.querySelectorAll("iframe"); iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html`; await waitForMessage("ready", `https://${iframe_domain}`); diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html index 23fd058c44..d8788edee9 100644 --- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html @@ -33,7 +33,7 @@ function createPopup() { window.addEventListener("load", createPopup); console.log("TKTK: Adding initial load"); -async function runTheTest(iframe_domain, cross_origin_domain, mode) { +async function runTheTest(iframe_domain, cross_origin_domain) { await new Promise(r => setTimeout(r, 2000)); console.log("TKTK: runTheTest() popup =", (popup === undefined ? "undefined" : "something")); if (document.readyState !== 'complete') { diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html index ae08111e61..ea38234def 100644 --- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html @@ -3,7 +3,7 @@ <script src="shared_test_funcs.js"></script> <script type="text/javascript"> var popup; -async function runTheTest(iframe_domain, cross_origin_domain, mode) { +async function runTheTest(iframe_domain, cross_origin_domain) { let s = `<html><script> console.log("TKTK: Loaded popup"); function give_result() { diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html index d03c514fc7..9c52f5774a 100644 --- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html @@ -5,7 +5,7 @@ <title></title> <script src="shared_test_funcs.js"></script> <script> -async function runTheTest(iframe_domain, cross_origin_domain) { +async function runTheTest(iframe_domain) { // Set up the frame const iframes = document.querySelectorAll("iframe"); iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframee.html`; diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html index 188d78ee6e..75ae15313b 100644 --- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html @@ -3,7 +3,7 @@ <script src="shared_test_funcs.js"></script> <script type="text/javascript"> var popup; -async function runTheTest(iframe_domain, cross_origin_domain, mode) { +async function runTheTest(iframe_domain, cross_origin_domain) { let s = `<!DOCTYPE html><html><script> function give_result() { return { diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html index 3de74bc9a3..b3eb2e6ad2 100644 --- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html @@ -5,7 +5,7 @@ <title></title> <script src="shared_test_funcs.js"></script> <script> -async function runTheTest(iframe_domain, cross_origin_domain, mode) { +async function runTheTest(iframe_domain, cross_origin_domain) { var child_reference; let url = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframee.html?mode=` let params = new URLSearchParams(document.location.search); diff --git a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html index 8a4373c703..f4ea70e466 100644 --- a/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html +++ b/browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html @@ -3,7 +3,7 @@ <body> <output id="result"></output> <script type="text/javascript"> - window.addEventListener("load", function listener(event) { + window.addEventListener("load", function listener() { parent.postMessage(["frame_ready"], "*"); }); window.addEventListener("message", function listener(event) { diff --git a/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html b/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html index 8e312d1d7b..350d05f6aa 100644 --- a/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html +++ b/browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html @@ -52,7 +52,7 @@ window.addEventListener("message", async function listener(event) { result.framee_crossOrigin_userAgentHTTPHeader = content; }); - Promise.all([one, two]).then((values) => { + Promise.all([one, two]).then(() => { parent.postMessage(result, "*") }); } diff --git a/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html index 4d9c81ec8d..499d9d8194 100644 --- a/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html +++ b/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html @@ -5,7 +5,7 @@ <title></title> <script src="shared_test_funcs.js"></script> <script> -async function runTheTest(iframe_domain, cross_origin_domain, extraData) { +async function runTheTest(iframe_domain, cross_origin_domain) { const iframes = document.querySelectorAll("iframe"); iframes[0].src = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframee.html`; await waitForMessage("ready", `https://${iframe_domain}`); diff --git a/browser/components/resistfingerprinting/test/browser/head.js b/browser/components/resistfingerprinting/test/browser/head.js index 3c3f588960..8973839220 100644 --- a/browser/components/resistfingerprinting/test/browser/head.js +++ b/browser/components/resistfingerprinting/test/browser/head.js @@ -372,9 +372,7 @@ async function testWindowOpen( aTargetWidth, aTargetHeight, aMaxAvailWidth, - aMaxAvailHeight, - aPopupChromeUIWidth, - aPopupChromeUIHeight + aMaxAvailHeight ) { // If the target size is greater than the maximum available content size, // we set the target size to it. @@ -687,7 +685,7 @@ async function runActualTest(uri, testFunction, expectedResults, extraData) { let filterExtraData = function (x) { let banned_keys = ["private_window", "etp_reload", "noopener", "await_uri"]; return Object.fromEntries( - Object.entries(x).filter(([k, v]) => !banned_keys.includes(k)) + Object.entries(x).filter(([k]) => !banned_keys.includes(k)) ); }; diff --git a/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html b/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html index 95394ddb56..1c8828ee9a 100644 --- a/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html +++ b/browser/components/resistfingerprinting/test/mochitest/test_geolocation.html @@ -30,7 +30,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1372069 function doTest_getCurrentPosition() { navigator.geolocation.getCurrentPosition( - (position) => { + () => { ok(true, "Success callback is expected to be called"); doTest_watchPosition(); }, @@ -43,7 +43,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1372069 function doTest_watchPosition() { let wid = navigator.geolocation.watchPosition( - (position) => { + () => { ok(true, "Success callback is expected to be called"); navigator.geolocation.clearWatch(wid); SimpleTest.finish(); diff --git a/browser/components/safebrowsing/content/test/browser_whitelisted.js b/browser/components/safebrowsing/content/test/browser_whitelisted.js index 92c42a5b52..eb217d618a 100644 --- a/browser/components/safebrowsing/content/test/browser_whitelisted.js +++ b/browser/components/safebrowsing/content/test/browser_whitelisted.js @@ -12,7 +12,7 @@ registerCleanupFunction(function () { } }); -function testBlockedPage(window) { +function testBlockedPage() { info("Non-whitelisted pages must be blocked"); ok(true, "about:blocked was shown"); } diff --git a/browser/components/safebrowsing/content/test/head.js b/browser/components/safebrowsing/content/test/head.js index 145833d010..ffbdb18d15 100644 --- a/browser/components/safebrowsing/content/test/head.js +++ b/browser/components/safebrowsing/content/test/head.js @@ -1,4 +1,4 @@ -// This url must sync with the table, url in SafeBrowsing.jsm addMozEntries +// This url must sync with the table, url in SafeBrowsing.sys.mjs addMozEntries const PHISH_TABLE = "moztest-phish-simple"; const PHISH_URL = "https://www.itisatrap.org/firefox/its-a-trap.html"; diff --git a/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs index bcb3199902..3718b6a4e0 100644 --- a/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs +++ b/browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs @@ -37,6 +37,7 @@ import { import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { ShortcutUtils } from "resource://gre/modules/ShortcutUtils.sys.mjs"; const STATES = { CROSSHAIRS: "crosshairs", @@ -49,7 +50,7 @@ const STATES = { const lazy = {}; ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => { - return new Localization(["browser/screenshotsOverlay.ftl"], true); + return new Localization(["browser/screenshots.ftl"], true); }); const SCREENSHOTS_LAST_SAVED_METHOD_PREF = @@ -79,13 +80,33 @@ export class ScreenshotsOverlay { #methodsUsed; get markup() { - let [cancel, instructions, download, copy] = - lazy.overlayLocalization.formatMessagesSync([ - { id: "screenshots-overlay-cancel-button" }, - { id: "screenshots-overlay-instructions" }, - { id: "screenshots-overlay-download-button" }, - { id: "screenshots-overlay-copy-button" }, - ]); + let accelString = ShortcutUtils.getModifierString("accel"); + let copyShorcut = accelString + this.copyKey; + let downloadShortcut = accelString + this.downloadKey; + + let [ + cancelLabel, + cancelAttributes, + instructions, + downloadLabel, + downloadAttributes, + copyLabel, + copyAttributes, + ] = lazy.overlayLocalization.formatMessagesSync([ + { id: "screenshots-cancel-button" }, + { id: "screenshots-component-cancel-button" }, + { id: "screenshots-instructions" }, + { id: "screenshots-component-download-button-label" }, + { + id: "screenshots-component-download-button", + args: { shortcut: downloadShortcut }, + }, + { id: "screenshots-component-copy-button-label" }, + { + id: "screenshots-component-copy-button", + args: { shortcut: copyShorcut }, + }, + ]); return ` <template> @@ -98,7 +119,7 @@ export class ScreenshotsOverlay { <div class="face"></div> </div> <div class="preview-instructions">${instructions.value}</div> - <button class="screenshots-button ghost-button" id="screenshots-cancel-button">${cancel.value}</button> + <button class="screenshots-button ghost-button" id="screenshots-cancel-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}">${cancelLabel.value}</button> </div> <div id="hover-highlight" hidden></div> <div id="selection-container" hidden> @@ -138,9 +159,9 @@ export class ScreenshotsOverlay { </div> <div id="buttons-container" hidden> <div class="buttons-wrapper"> - <button id="cancel" class="screenshots-button" title="${cancel.value}" aria-label="${cancel.value}" tabindex="0"><img/></button> - <button id="copy" class="screenshots-button" title="${copy.value}" aria-label="${copy.value}" tabindex="0"><img/>${copy.value}</button> - <button id="download" class="screenshots-button primary" title="${download.value}" aria-label="${download.value}" tabindex="0"><img/>${download.value}</button> + <button id="cancel" class="screenshots-button" title="${cancelAttributes.attributes[0].value}" aria-label="${cancelAttributes.attributes[1].value}"><img/></button> + <button id="copy" class="screenshots-button" title="${copyAttributes.attributes[0].value}" aria-label="${copyAttributes.attributes[1].value}"><img/><label>${copyLabel.value}</label></button> + <button id="download" class="screenshots-button primary" title="${downloadAttributes.attributes[0].value}" aria-label="${downloadAttributes.attributes[1].value}"><img/><label>${downloadLabel.value}</label></button> </div> </div> </div> @@ -180,6 +201,14 @@ export class ScreenshotsOverlay { this.selectionRegion = new Region(this.windowDimensions); this.hoverElementRegion = new Region(this.windowDimensions); this.resetMethodsUsed(); + + let [downloadKey, copyKey] = lazy.overlayLocalization.formatMessagesSync([ + { id: "screenshots-component-download-key" }, + { id: "screenshots-component-copy-key" }, + ]); + + this.downloadKey = downloadKey.value; + this.copyKey = copyKey.value; } get content() { @@ -204,6 +233,9 @@ export class ScreenshotsOverlay { this.#content.root.appendChild(this.fragment); this.initializeElements(); + this.screenshotsContainer.dir = Services.locale.isAppLocaleRTL + ? "rtl" + : "ltr"; await this.updateWindowDimensions(); this.#setState(STATES.CROSSHAIRS); @@ -290,10 +322,6 @@ export class ScreenshotsOverlay { } handleEvent(event) { - if (event.button > 0) { - return; - } - switch (event.type) { case "click": this.handleClick(event); @@ -316,21 +344,46 @@ export class ScreenshotsOverlay { } } + /** + * If the event came from the primary button, return false as we should not + * early return in the event handler function. + * If the event had another button, set to the crosshairs or selected state + * and return true to early return from the event handler function. + * @param {PointerEvent} event + * @returns true if the event button(s) was the non primary button + * false otherwise + */ + preEventHandler(event) { + if (event.button > 0 || event.buttons > 1) { + switch (this.#state) { + case STATES.DRAGGING_READY: + this.#setState(STATES.CROSSHAIRS); + break; + case STATES.DRAGGING: + case STATES.RESIZING: + this.#setState(STATES.SELECTED); + break; + } + return true; + } + return false; + } + handleClick(event) { + if (this.preEventHandler(event)) { + return; + } + switch (event.originalTarget.id) { case "screenshots-cancel-button": case "cancel": this.maybeCancelScreenshots(); break; case "copy": - this.#dispatchEvent("Screenshots:Copy", { - region: this.selectionRegion.dimensions, - }); + this.copySelectedRegion(); break; case "download": - this.#dispatchEvent("Screenshots:Download", { - region: this.selectionRegion.dimensions, - }); + this.downloadSelectedRegion(); break; } } @@ -351,6 +404,16 @@ export class ScreenshotsOverlay { * @param {Event} event The pointerown event */ handlePointerDown(event) { + // Early return if the event target is not within the screenshots component + // element. + if (!event.originalTarget.closest("#screenshots-component")) { + return; + } + + if (this.preEventHandler(event)) { + return; + } + if ( event.originalTarget.id === "screenshots-cancel-button" || event.originalTarget.closest("#buttons-container") === @@ -379,6 +442,10 @@ export class ScreenshotsOverlay { * @param {Event} event The pointermove event */ handlePointerMove(event) { + if (this.preEventHandler(event)) { + return; + } + const { pageX, pageY, clientX, clientY } = this.getCoordinatesFromEvent(event); @@ -450,6 +517,18 @@ export class ScreenshotsOverlay { case "Escape": this.maybeCancelScreenshots(); break; + case this.copyKey.toLowerCase(): + if (this.state === "selected" && this.getAccelKey(event)) { + event.preventDefault(); + this.copySelectedRegion(); + } + break; + case this.downloadKey.toLowerCase(): + if (this.state === "selected" && this.getAccelKey(event)) { + event.preventDefault(); + this.downloadSelectedRegion(); + } + break; } } @@ -780,9 +859,9 @@ export class ScreenshotsOverlay { */ setFocusToActionButton() { if (lazy.SCREENSHOTS_LAST_SAVED_METHOD === "copy") { - this.copyButton.focus({ focusVisible: true }); + this.copyButton.focus({ focusVisible: true, preventScroll: true }); } else { - this.downloadButton.focus({ focusVisible: true }); + this.downloadButton.focus({ focusVisible: true, preventScroll: true }); } } @@ -868,6 +947,18 @@ export class ScreenshotsOverlay { } } + copySelectedRegion() { + this.#dispatchEvent("Screenshots:Copy", { + region: this.selectionRegion.dimensions, + }); + } + + downloadSelectedRegion() { + this.#dispatchEvent("Screenshots:Download", { + region: this.selectionRegion.dimensions, + }); + } + /** * Hide hover element, selection and buttons containers. * Show the preview container and the panel. @@ -1285,17 +1376,21 @@ export class ScreenshotsOverlay { this.updateSelectionSizeText(); } + /** + * Update the size of the selected region. Use the zoom to correctly display + * the region dimensions. + */ updateSelectionSizeText() { - let dpr = this.windowDimensions.devicePixelRatio; let { width, height } = this.selectionRegion.dimensions; + let zoom = Math.round(this.window.browsingContext.fullZoom * 100) / 100; let [selectionSizeTranslation] = lazy.overlayLocalization.formatMessagesSync([ { - id: "screenshots-overlay-selection-region-size", + id: "screenshots-overlay-selection-region-size-2", args: { - width: Math.floor(width * dpr), - height: Math.floor(height * dpr), + width: Math.floor(width * zoom), + height: Math.floor(height * zoom), }, }, ]); diff --git a/browser/components/screenshots/ScreenshotsUtils.sys.mjs b/browser/components/screenshots/ScreenshotsUtils.sys.mjs index fc84facee3..9df74a4359 100644 --- a/browser/components/screenshots/ScreenshotsUtils.sys.mjs +++ b/browser/components/screenshots/ScreenshotsUtils.sys.mjs @@ -817,8 +817,9 @@ export var ScreenshotsUtils = { let dialog = await this.openPreviewDialog(browser); await dialog._dialogReady; - let screenshotsUI = - dialog._frame.contentDocument.createElement("screenshots-ui"); + let screenshotsUI = dialog._frame.contentDocument.createElement( + "screenshots-preview" + ); dialog._frame.contentDocument.body.appendChild(screenshotsUI); screenshotsUI.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD); diff --git a/browser/components/screenshots/content/screenshots.css b/browser/components/screenshots/content/screenshots.css index 506f3658c9..b155c294f8 100644 --- a/browser/components/screenshots/content/screenshots.css +++ b/browser/components/screenshots/content/screenshots.css @@ -30,13 +30,12 @@ body { display: flex; align-items: center; justify-content: center; + gap: var(--space-xsmall); cursor: pointer; text-align: center; user-select: none; white-space: nowrap; - min-height: 36px; - font-size: 15px; - min-width: 36px; + min-width: 32px; } .preview-button > img { @@ -44,11 +43,23 @@ body { fill: currentColor; width: 16px; height: 16px; + pointer-events: none; +} + +#retry > img { + content: url("chrome://global/skin/icons/reload.svg"); +} + +#cancel > img { + content: url("chrome://global/skin/icons/close.svg"); } -#download > img, #copy > img { - margin-inline-end: 5px; + content: url("chrome://global/skin/icons/edit-copy.svg"); +} + +#download > img { + content: url("chrome://browser/skin/downloads/downloads.svg"); } .preview-image { diff --git a/browser/components/screenshots/content/screenshots.html b/browser/components/screenshots/content/screenshots.html index 88c71fb4fe..fea032700c 100644 --- a/browser/components/screenshots/content/screenshots.html +++ b/browser/components/screenshots/content/screenshots.html @@ -31,32 +31,34 @@ <button id="retry" class="preview-button" - data-l10n-id="screenshots-retry-button-title" + data-l10n-id="screenshots-component-retry-button" > - <img src="chrome://global/skin/icons/reload.svg" /> + <img /> </button> <button id="cancel" class="preview-button" - data-l10n-id="screenshots-cancel-button-title" + data-l10n-id="screenshots-component-cancel-button" > - <img src="chrome://global/skin/icons/close.svg" /> + <img /> </button> <button id="copy" class="preview-button" - data-l10n-id="screenshots-copy-button-title" + data-l10n-id="screenshots-component-copy-button" > - <img src="chrome://global/skin/icons/edit-copy.svg" /> - <span data-l10n-id="screenshots-copy-button" /> + <img /><label + data-l10n-id="screenshots-component-copy-button-label" + ></label> </button> <button id="download" class="preview-button primary" - data-l10n-id="screenshots-download-button-title" + data-l10n-id="screenshots-component-download-button" > - <img src="chrome://browser/skin/downloads/downloads.svg" /> - <span data-l10n-id="screenshots-download-button" /> + <img /><label + data-l10n-id="screenshots-component-download-button-label" + ></label> </button> </div> <div class="preview-image"> diff --git a/browser/components/screenshots/content/screenshots.js b/browser/components/screenshots/content/screenshots.js index 9e47570e07..8159206d18 100644 --- a/browser/components/screenshots/content/screenshots.js +++ b/browser/components/screenshots/content/screenshots.js @@ -6,15 +6,35 @@ "use strict"; ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", + ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs", }); -class ScreenshotsUI extends HTMLElement { +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "screenshotsLocalization", () => { + return new Localization(["browser/screenshots.ftl"], true); +}); + +class ScreenshotsPreview extends HTMLElement { constructor() { super(); // we get passed the <browser> as a param via TabDialogBox.open() this.openerBrowser = window.arguments[0]; + + window.ensureCustomElements("moz-button"); + + let [downloadKey, copyKey] = + lazy.screenshotsLocalization.formatMessagesSync([ + { id: "screenshots-component-download-key" }, + { id: "screenshots-component-copy-key" }, + ]); + + this.downloadKey = downloadKey.value; + this.copyKey = copyKey.value; } + async connectedCallback() { this.initialize(); } @@ -38,6 +58,29 @@ class ScreenshotsUI extends HTMLElement { this._copyButton.addEventListener("click", this); this._downloadButton = this.querySelector("#download"); this._downloadButton.addEventListener("click", this); + + let accelString = ShortcutUtils.getModifierString("accel"); + let copyShorcut = accelString + this.copyKey; + let downloadShortcut = accelString + this.downloadKey; + + document.l10n.setAttributes( + this._cancelButton, + "screenshots-component-cancel-button" + ); + + document.l10n.setAttributes( + this._copyButton, + "screenshots-component-copy-button", + { shortcut: copyShorcut } + ); + + document.l10n.setAttributes( + this._downloadButton, + "screenshots-component-download-button", + { shortcut: downloadShortcut } + ); + + window.addEventListener("keydown", this, true); } close() { @@ -45,31 +88,68 @@ class ScreenshotsUI extends HTMLElement { window.close(); } - async handleEvent(event) { - if (event.type == "click" && event.currentTarget == this._cancelButton) { - this.close(); - ScreenshotsUtils.recordTelemetryEvent("canceled", "preview_cancel", {}); - } else if ( - event.type == "click" && - event.currentTarget == this._copyButton - ) { - this.saveToClipboard( - this.ownerDocument.getElementById("placeholder-image").src - ); - } else if ( - event.type == "click" && - event.currentTarget == this._downloadButton - ) { - await this.saveToFile( - this.ownerDocument.getElementById("placeholder-image").src - ); - } else if ( - event.type == "click" && - event.currentTarget == this._retryButton - ) { - ScreenshotsUtils.scheduleRetry(this.openerBrowser, "preview_retry"); - this.close(); + handleEvent(event) { + switch (event.type) { + case "click": + this.handleClick(event); + break; + case "keydown": + this.handleKeydown(event); + break; + } + } + + handleClick(event) { + switch (event.target.id) { + case "retry": + ScreenshotsUtils.scheduleRetry(this.openerBrowser, "preview_retry"); + this.close(); + break; + case "cancel": + this.close(); + ScreenshotsUtils.recordTelemetryEvent("canceled", "preview_cancel", {}); + break; + case "copy": + this.saveToClipboard( + this.ownerDocument.getElementById("placeholder-image").src + ); + break; + case "download": + this.saveToFile( + this.ownerDocument.getElementById("placeholder-image").src + ); + break; + } + } + + handleKeydown(event) { + switch (event.key) { + case this.copyKey.toLowerCase(): + if (this.getAccelKey(event)) { + event.preventDefault(); + event.stopPropagation(); + this.saveToClipboard( + this.ownerDocument.getElementById("placeholder-image").src + ); + } + break; + case this.downloadKey.toLowerCase(): + if (this.getAccelKey(event)) { + event.preventDefault(); + event.stopPropagation(); + this.saveToFile( + this.ownerDocument.getElementById("placeholder-image").src + ); + } + break; + } + } + + getAccelKey(event) { + if (AppConstants.platform === "macosx") { + return event.metaKey; } + return event.ctrlKey; } async saveToFile(dataUrl) { @@ -102,4 +182,4 @@ class ScreenshotsUI extends HTMLElement { } } } -customElements.define("screenshots-ui", ScreenshotsUI); +customElements.define("screenshots-preview", ScreenshotsPreview); diff --git a/browser/components/screenshots/overlay/overlay.css b/browser/components/screenshots/overlay/overlay.css index 6eeda8b44c..b042f0b0c2 100644 --- a/browser/components/screenshots/overlay/overlay.css +++ b/browser/components/screenshots/overlay/overlay.css @@ -6,6 +6,12 @@ :host { display: contents; + + /* These z-indexes are used to correctly layer elements in the screenshots overlay */ + --screenshots-lowest-layer: 1; + --screenshots-low-layer: 2; + --screenshots-high-layer: 3; + --screenshots-highest-layer: 4; } [hidden] { @@ -57,6 +63,7 @@ position: absolute; margin: 10px 0; cursor: auto; + z-index: var(--screenshots-highest-layer); } #selection-size, @@ -77,11 +84,13 @@ .screenshots-button { display: inline-flex; align-items: center; + justify-content: center; + gap: var(--space-xsmall); cursor: pointer; text-align: center; user-select: none; white-space: nowrap; - z-index: 6; + z-index: var(--screenshots-highest-layer); min-width: 32px; margin-inline: 4px; } @@ -90,6 +99,7 @@ width: 100%; height: 100%; pointer-events: none; + z-index: var(--screenshots-lowest-layer); } #screenshots-cancel-button { @@ -123,6 +133,10 @@ pointer-events: none; } +.screenshots-button > label { + pointer-events: none; +} + #cancel > img { content: url("chrome://global/skin/icons/close.svg"); } @@ -135,15 +149,14 @@ content: url("chrome://browser/skin/downloads/downloads.svg"); } -#download > img, -#copy > img { - margin-inline-end: 5px; -} - .face-container { position: relative; width: 64px; height: 64px; + + @media (prefers-contrast) { + display: none; + } } .face { @@ -172,7 +185,7 @@ border-radius: 50%; inset-inline-start: 2px; top: 4px; - z-index: 10; + z-index: var(--screenshots-high-layer); } .left { @@ -209,7 +222,7 @@ box-sizing: border-box; pointer-events: none; position: absolute; - z-index: 11; + z-index: var(--screenshots-high-layer); } #top-background { @@ -242,7 +255,7 @@ cursor: move; position: absolute; pointer-events: auto; - z-index: 2; + z-index: var(--screenshots-lowest-layer); outline-offset: 8px; } @@ -251,7 +264,7 @@ align-items: center; justify-content: center; position: absolute; - z-index: 5; + z-index: var(--screenshots-high-layer); pointer-events: auto; outline-offset: -15px; } @@ -270,7 +283,7 @@ inset-inline-start: 0; top: -30px; width: 100%; - z-index: 4; + z-index: var(--screenshots-low-layer); } .mover-target.direction-topRight { @@ -287,7 +300,7 @@ left: -30px; top: 0; width: 60px; - z-index: 4; + z-index: var(--screenshots-low-layer); } .mover-target.direction-right { @@ -296,7 +309,7 @@ right: -30px; top: 0; width: 60px; - z-index: 4; + z-index: var(--screenshots-low-layer); } .mover-target.direction-bottomLeft { @@ -313,7 +326,7 @@ height: 60px; inset-inline-start: 0; width: 100%; - z-index: 4; + z-index: var(--screenshots-low-layer); } .mover-target.direction-bottomRight { diff --git a/browser/components/screenshots/screenshots-buttons.css b/browser/components/screenshots/screenshots-buttons.css index 82b075bccb..b63308d8b4 100644 --- a/browser/components/screenshots/screenshots-buttons.css +++ b/browser/components/screenshots/screenshots-buttons.css @@ -25,7 +25,8 @@ .full-page, .visible-page { -moz-context-properties: fill, stroke; fill: currentColor; - stroke: var(--color-accent-primary); + /* stroke is the secondary fill color used to define the viewport shape in the SVGs */ + stroke: var(--color-gray-60); background-position: center top; background-repeat: no-repeat; background-size: 46px 46px; diff --git a/browser/components/screenshots/screenshots-buttons.js b/browser/components/screenshots/screenshots-buttons.js index 864505ae2f..9ac8dab2cf 100644 --- a/browser/components/screenshots/screenshots-buttons.js +++ b/browser/components/screenshots/screenshots-buttons.js @@ -13,28 +13,40 @@ }); class ScreenshotsButtons extends MozXULElement { + static #template = null; + static get markup() { return ` - <html:link rel="stylesheet" href="chrome://global/skin/global.css"/> - <html:link rel="stylesheet" href="chrome://browser/content/screenshots/screenshots-buttons.css"/> - <html:button class="visible-page footer-button" data-l10n-id="screenshots-save-visible-button"></html:button> - <html:button class="full-page footer-button" data-l10n-id="screenshots-save-page-button"></html:button> + <html:link rel="stylesheet" href="chrome://global/skin/global.css" /> + <html:link rel="stylesheet" href="chrome://browser/content/screenshots/screenshots-buttons.css" /> + <html:moz-button-group> + <html:button class="visible-page footer-button" data-l10n-id="screenshots-save-visible-button"></html:button> + <html:button class="full-page footer-button primary" data-l10n-id="screenshots-save-page-button"></html:button> + </html:moz-button-group> `; } + static get fragment() { + if (!ScreenshotsButtons.#template) { + ScreenshotsButtons.#template = MozXULElement.parseXULToFragment( + ScreenshotsButtons.markup + ); + } + return ScreenshotsButtons.#template; + } + connectedCallback() { const shadowRoot = this.attachShadow({ mode: "open" }); document.l10n.connectRoot(shadowRoot); - let fragment = MozXULElement.parseXULToFragment(this.constructor.markup); - this.shadowRoot.append(fragment); + this.shadowRoot.append(ScreenshotsButtons.fragment); - let visibleButton = shadowRoot.querySelector(".visible-page"); + let visibleButton = this.shadowRoot.querySelector(".visible-page"); visibleButton.onclick = function () { ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "visible"); }; - let fullpageButton = shadowRoot.querySelector(".full-page"); + let fullpageButton = this.shadowRoot.querySelector(".full-page"); fullpageButton.onclick = function () { ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "full_page"); }; @@ -49,7 +61,8 @@ * This will default to the visible page button. * @param {String} buttonToFocus */ - focusButton(buttonToFocus) { + async focusButton(buttonToFocus) { + await this.shadowRoot.querySelector("moz-button-group").updateComplete; if (buttonToFocus === "fullpage") { this.shadowRoot .querySelector(".full-page") diff --git a/browser/components/screenshots/tests/browser/browser.toml b/browser/components/screenshots/tests/browser/browser.toml index b27d28c677..97e7474fa3 100644 --- a/browser/components/screenshots/tests/browser/browser.toml +++ b/browser/components/screenshots/tests/browser/browser.toml @@ -18,6 +18,8 @@ prefs = [ ["browser_iframe_test.js"] skip-if = ["os == 'linux'"] +["browser_keyboard_shortcuts.js"] + ["browser_overlay_keyboard_test.js"] ["browser_screenshots_drag_scroll_test.js"] @@ -63,3 +65,5 @@ skip-if = ["!crashreporter"] ["browser_test_moving_tab_to_new_window.js"] ["browser_test_resize.js"] + +["browser_test_selection_size_text.js"] diff --git a/browser/components/screenshots/tests/browser/browser_iframe_test.js b/browser/components/screenshots/tests/browser/browser_iframe_test.js index bb853fbe28..24f7a71dca 100644 --- a/browser/components/screenshots/tests/browser/browser_iframe_test.js +++ b/browser/components/screenshots/tests/browser/browser_iframe_test.js @@ -90,7 +90,7 @@ add_task(async function test_selectingElementsInIframes() { await helper.waitForHoverElementRect(el.width, el.height); mouse.click(x, y); - await helper.waitForStateChange("selected"); + await helper.waitForStateChange(["selected"]); let dimensions = await helper.getSelectionRegionDimensions(); @@ -116,7 +116,7 @@ add_task(async function test_selectingElementsInIframes() { ); mouse.click(500, 500); - await helper.waitForStateChange("crosshairs"); + await helper.waitForStateChange(["crosshairs"]); } } ); diff --git a/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js b/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js new file mode 100644 index 0000000000..bca96f333f --- /dev/null +++ b/browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_download_shortcut() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.download.useDownloadDir", true]], + }); + + let publicDownloads = await Downloads.getList(Downloads.PUBLIC); + // First ensure we catch the download finishing. + let downloadFinishedPromise = new Promise(resolve => { + publicDownloads.addView({ + onDownloadChanged(download) { + info("Download changed!"); + if (download.succeeded || download.error) { + info("Download succeeded or errored"); + publicDownloads.removeView(this); + resolve(download); + } + }, + }); + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + await helper.dragOverlay(10, 10, 500, 500); + + let screenshotExit = TestUtils.topicObserved("screenshots-exit"); + + await SpecialPowers.spawn(browser, [], async () => { + EventUtils.synthesizeKey("s", { accelKey: true }, content); + }); + + info("wait for download to finish"); + let download = await downloadFinishedPromise; + + ok(download.succeeded, "Download should succeed"); + + await publicDownloads.removeFinished(); + await screenshotExit; + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + let screenshotReady = TestUtils.topicObserved( + "screenshots-preview-ready" + ); + + let visibleButton = await helper.getPanelButton(".visible-page"); + visibleButton.click(); + + await screenshotReady; + + screenshotExit = TestUtils.topicObserved("screenshots-exit"); + + EventUtils.synthesizeKey("s", { accelKey: true }); + + info("wait for download to finish"); + download = await downloadFinishedPromise; + + ok(download.succeeded, "Download should succeed"); + + await publicDownloads.removeFinished(); + await screenshotExit; + } + ); +}); + +add_task(async function test_copy_shortcut() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + let contentInfo = await helper.getContentDimensions(); + ok(contentInfo, "Got dimensions back from the content"); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + await helper.dragOverlay(10, 10, 500, 500); + + let screenshotExit = TestUtils.topicObserved("screenshots-exit"); + let clipboardChanged = helper.waitForRawClipboardChange(490, 490); + + await SpecialPowers.spawn(browser, [], async () => { + EventUtils.synthesizeKey("c", { accelKey: true }, content); + }); + + await clipboardChanged; + await screenshotExit; + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + let screenshotReady = TestUtils.topicObserved( + "screenshots-preview-ready" + ); + + let visibleButton = await helper.getPanelButton(".visible-page"); + visibleButton.click(); + + await screenshotReady; + + clipboardChanged = helper.waitForRawClipboardChange( + contentInfo.clientWidth, + contentInfo.clientHeight + ); + screenshotExit = TestUtils.topicObserved("screenshots-exit"); + + EventUtils.synthesizeKey("c", { accelKey: true }); + + await clipboardChanged; + await screenshotExit; + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js index 757d721268..86940a5203 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js @@ -353,8 +353,6 @@ add_task(async function test_scrollIfByEdge() { await helper.scrollContentWindow(windowX, windowY); - await TestUtils.waitForTick(); - helper.triggerUIFromToolbar(); await helper.waitForOverlay(); @@ -363,17 +361,18 @@ add_task(async function test_scrollIfByEdge() { is(scrollX, windowX, "Window x position is 1000"); is(scrollY, windowY, "Window y position is 1000"); - let startX = 1100; - let startY = 1100; + let startX = 1200; + let startY = 1200; let endX = 1010; let endY = 1010; - // The window won't scroll if the state is draggingReady so we move to - // get into the dragging state and then move again to scroll the window - mouse.down(startX, startY); - await helper.assertStateChange("draggingReady"); - mouse.move(1050, 1050); - await helper.assertStateChange("dragging"); + await helper.dragOverlay(startX, startY, endX + 20, endY + 20); + await helper.scrollContentWindow(windowX, windowY); + + await TestUtils.waitForTick(); + + mouse.down(endX + 20, endY + 20); + await helper.assertStateChange("resizing"); mouse.move(endX, endY); mouse.up(endX, endY); await helper.assertStateChange("selected"); @@ -387,26 +386,64 @@ add_task(async function test_scrollIfByEdge() { is(scrollX, windowX, "Window x position is 990"); is(scrollY, windowY, "Window y position is 990"); + let screenshotExit = TestUtils.topicObserved("screenshots-exit"); + helper.triggerUIFromToolbar(); + await screenshotExit; + } + ); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + + let windowX = 1000; + let windowY = 1000; + + await helper.scrollContentWindow(windowX, windowY); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + let contentInfo = await helper.getContentDimensions(); + let { scrollX, scrollY, clientWidth, clientHeight } = contentInfo; + + let startX = windowX + clientWidth - 200; + let startY = windowX + clientHeight - 200; + let endX = windowX + clientWidth - 10; + let endY = windowY + clientHeight - 10; - endX = windowX + contentInfo.clientWidth - 10; - endY = windowY + contentInfo.clientHeight - 10; + await helper.dragOverlay(startX, startY, endX - 20, endY - 20); + await helper.scrollContentWindow(windowX, windowY); + + await TestUtils.waitForTick(); info( `starting to drag overlay to ${endX}, ${endY} in test\nclientInfo: ${JSON.stringify( contentInfo )}\n` ); - await helper.dragOverlay(startX, startY, endX, endY, "selected"); + mouse.down(endX - 20, endY - 20); + await helper.assertStateChange("resizing"); + mouse.move(endX, endY); + mouse.up(endX, endY); + await helper.assertStateChange("selected"); - windowX = 1000; - windowY = 1000; + windowX = 1010; + windowY = 1010; await helper.waitForScrollTo(windowX, windowY); ({ scrollX, scrollY } = await helper.getContentDimensions()); - is(scrollX, windowX, "Window x position is 1000"); - is(scrollY, windowY, "Window y position is 1000"); + is(scrollX, windowX, "Window x position is 1010"); + is(scrollY, windowY, "Window y position is 1010"); + + let screenshotExit = TestUtils.topicObserved("screenshots-exit"); + helper.triggerUIFromToolbar(); + await screenshotExit; } ); }); @@ -428,13 +465,19 @@ add_task(async function test_scrollIfByEdgeWithKeyboard() { helper.triggerUIFromToolbar(); await helper.waitForOverlay(); - let { scrollX, scrollY, clientWidth, clientHeight } = - await helper.getContentDimensions(); + let { scrollX, scrollY } = await helper.getContentDimensions(); is(scrollX, windowX, "Window x position is 1000"); is(scrollY, windowY, "Window y position is 1000"); - await helper.dragOverlay(1020, 1020, 1120, 1120); + await helper.dragOverlay( + scrollX + 20, + scrollY + 20, + scrollX + 120, + scrollY + 120 + ); + + await helper.scrollContentWindow(windowX, windowY); await helper.moveOverlayViaKeyboard("highlight", [ { key: "ArrowLeft", options: { shiftKey: true } }, @@ -447,14 +490,36 @@ add_task(async function test_scrollIfByEdgeWithKeyboard() { windowY = 989; await helper.waitForScrollTo(windowX, windowY); - ({ scrollX, scrollY, clientWidth, clientHeight } = - await helper.getContentDimensions()); + ({ scrollX, scrollY } = await helper.getContentDimensions()); is(scrollX, windowX, "Window x position is 989"); is(scrollY, windowY, "Window y position is 989"); - mouse.click(1200, 1200); - await helper.assertStateChange("crosshairs"); + let screenshotExit = TestUtils.topicObserved("screenshots-exit"); + helper.triggerUIFromToolbar(); + await screenshotExit; + } + ); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + + let windowX = 989; + let windowY = 989; + + await helper.scrollContentWindow(windowX, windowY); + + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + let { scrollX, scrollY, clientWidth, clientHeight } = + await helper.getContentDimensions(); + await helper.dragOverlay( scrollX + clientWidth - 100 - 20, scrollY + clientHeight - 100 - 20, @@ -477,6 +542,10 @@ add_task(async function test_scrollIfByEdgeWithKeyboard() { is(scrollX, windowX, "Window x position is 1000"); is(scrollY, windowY, "Window y position is 1000"); + + let screenshotExit = TestUtils.topicObserved("screenshots-exit"); + helper.triggerUIFromToolbar(); + await screenshotExit; } ); }); diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js index 605e0ae75c..3cef2dbd72 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js @@ -442,7 +442,7 @@ add_task(async function resizeAllCorners() { /** * This function tests clicking the overlay with the different mouse buttons */ -add_task(async function test_otherMouseButtons() { +add_task(async function test_clickingOtherMouseButtons() { await BrowserTestUtils.withNewTab( { gBrowser, @@ -478,6 +478,7 @@ add_task(async function test_otherMouseButtons() { mouse.down(10, 10, { button: 2 }); mouse.move(100, 100, { button: 2 }); + mouse.up(100, 100, { button: 2 }); await TestUtils.waitForTick(); @@ -486,3 +487,66 @@ add_task(async function test_otherMouseButtons() { } ); }); + +/** + * This function tests dragging the overlay with the different mouse buttons + */ +add_task(async function test_draggingOtherMouseButtons() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + helper.triggerUIFromToolbar(); + await helper.waitForOverlay(); + + // Click with button 1 in dragging state + mouse.down(100, 100); + await helper.assertStateChange("draggingReady"); + mouse.move(200, 200); + await helper.assertStateChange("dragging"); + mouse.click(200, 200, { button: 1 }); + await helper.assertStateChange("selected"); + + // Reset + mouse.click(10, 10); + await helper.assertStateChange("crosshairs"); + + // Mouse down with button 2 in draggingReady state + mouse.down(100, 100); + await helper.assertStateChange("draggingReady"); + mouse.down(200, 200, { button: 2 }); + await helper.assertStateChange("crosshairs"); + + await helper.dragOverlay(100, 100, 200, 200); + + // Click with button 1 in resizing state + mouse.down(200, 200); + await helper.assertStateChange("resizing"); + mouse.click(200, 200, { button: 1 }); + + // Reset + mouse.click(10, 10); + await helper.assertStateChange("crosshairs"); + + await helper.dragOverlay(100, 100, 200, 200); + + // Mouse down with button 2 in dragging state + mouse.down(200, 200); + await helper.assertStateChange("resizing"); + mouse.down(200, 200, { button: 2 }); + + // Reset + mouse.click(10, 10); + await helper.assertStateChange("crosshairs"); + + // Mouse move with button 2 in draggingReady state + mouse.down(100, 100); + await helper.assertStateChange("draggingReady"); + mouse.move(100, 100, { button: 2 }); + await helper.assertStateChange("crosshairs"); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js index ad262a7e67..021a37b5c9 100644 --- a/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js +++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js @@ -31,7 +31,7 @@ add_task(async function test_toggling_screenshots_pref() { .callsFake(observerSpy); let notifierStub = sinon .stub(ScreenshotsUtils, "notify") - .callsFake(function (window, type) { + .callsFake(function () { notifierSpy(); ScreenshotsUtils.notify.wrappedMethod.apply(this, arguments); }); diff --git a/browser/components/screenshots/tests/browser/browser_test_element_picker.js b/browser/components/screenshots/tests/browser/browser_test_element_picker.js index 17ed2a0190..3e2069134e 100644 --- a/browser/components/screenshots/tests/browser/browser_test_element_picker.js +++ b/browser/components/screenshots/tests/browser/browser_test_element_picker.js @@ -43,14 +43,14 @@ add_task(async function test_element_picker() { ); mouse.click(10, 10); - await helper.waitForStateChange("crosshairs"); + await helper.waitForStateChange(["crosshairs"]); let hoverElementRegionValid = await helper.isHoverElementRegionValid(); ok(!hoverElementRegionValid, "Hover element rect is null"); mouse.click(10, 10); - await helper.waitForStateChange("crosshairs"); + await helper.waitForStateChange(["crosshairs"]); } ); }); diff --git a/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js b/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js new file mode 100644 index 0000000000..38d1acbea9 --- /dev/null +++ b/browser/components/screenshots/tests/browser/browser_test_selection_size_text.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_selectionSizeTest() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + const dpr = browser.ownerGlobal.devicePixelRatio; + let helper = new ScreenshotsHelper(browser); + + helper.triggerUIFromToolbar(); + + await helper.waitForOverlay(); + await helper.dragOverlay(100, 100, 500, 500); + + let actualText = await helper.getOverlaySelectionSizeText(); + + Assert.equal( + actualText, + `${400 * dpr} x ${400 * dpr}`, + "The selection size text is the same" + ); + } + ); +}); + +add_task(async function test_selectionSizeTestAt1Point5Zoom() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + const zoom = 1.5; + const dpr = browser.ownerGlobal.devicePixelRatio; + let helper = new ScreenshotsHelper(browser); + helper.zoomBrowser(zoom); + + helper.triggerUIFromToolbar(); + + await helper.waitForOverlay(); + await helper.dragOverlay(100, 100, 500, 500); + + let actualText = await helper.getOverlaySelectionSizeText(); + + Assert.equal( + actualText, + `${400 * dpr * zoom} x ${400 * dpr * zoom}`, + "The selection size text is the same" + ); + } + ); +}); + +add_task(async function test_selectionSizeTestAtPoint5Zoom() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + }, + async browser => { + const zoom = 0.5; + const dpr = browser.ownerGlobal.devicePixelRatio; + let helper = new ScreenshotsHelper(browser); + helper.zoomBrowser(zoom); + + helper.triggerUIFromToolbar(); + + await helper.waitForOverlay(); + await helper.dragOverlay(100, 100, 500, 500); + + let actualText = await helper.getOverlaySelectionSizeText(); + + Assert.equal( + actualText, + `${400 * dpr * zoom} x ${400 * dpr * zoom}`, + "The selection size text is the same" + ); + } + ); +}); diff --git a/browser/components/screenshots/tests/browser/head.js b/browser/components/screenshots/tests/browser/head.js index a36e955830..762da5f866 100644 --- a/browser/components/screenshots/tests/browser/head.js +++ b/browser/components/screenshots/tests/browser/head.js @@ -159,23 +159,23 @@ class ScreenshotsHelper { }); } - waitForStateChange(newState) { - return SpecialPowers.spawn(this.browser, [newState], async state => { + waitForStateChange(newStateArr) { + return SpecialPowers.spawn(this.browser, [newStateArr], async stateArr => { let screenshotsChild = content.windowGlobalChild.getActor( "ScreenshotsComponent" ); await ContentTaskUtils.waitForCondition(() => { - info(`got ${screenshotsChild.overlay.state}. expected ${state}`); - return screenshotsChild.overlay.state === state; - }, `Wait for overlay state to be ${state}`); + info(`got ${screenshotsChild.overlay.state}. expected ${stateArr}`); + return stateArr.includes(screenshotsChild.overlay.state); + }, `Wait for overlay state to be ${stateArr}`); return screenshotsChild.overlay.state; }); } async assertStateChange(newState) { - let currentState = await this.waitForStateChange(newState); + let currentState = await this.waitForStateChange([newState]); is( currentState, @@ -269,18 +269,13 @@ class ScreenshotsHelper { mouse.down(startX, startY); - await Promise.any([ - this.waitForStateChange("draggingReady"), - this.waitForStateChange("resizing"), - ]); + await this.waitForStateChange(["draggingReady", "resizing"]); Assert.ok(true, "The overlay is in the draggingReady or resizing state"); mouse.move(endX, endY); - await Promise.any([ - this.waitForStateChange("dragging"), - this.waitForStateChange("resizing"), - ]); + await this.waitForStateChange(["dragging", "resizing"]); + Assert.ok(true, "The overlay is in the dragging or resizing state"); // We intentionally turn off this a11y check, because the following mouse // event is emitted at the end of the dragging event. Its keyboard @@ -324,7 +319,6 @@ class ScreenshotsHelper { overlay.topRightMover.focus({ focusVisible: true }); break; } - screenshotsChild.overlay.highlightEl.focus(); for (let event of eventsArr) { EventUtils.synthesizeKey( @@ -354,7 +348,6 @@ class ScreenshotsHelper { } async scrollContentWindow(x, y) { - let promise = BrowserTestUtils.waitForContentEvent(this.browser, "scroll"); let contentDims = await this.getContentDimensions(); await ContentTask.spawn( this.browser, @@ -404,7 +397,6 @@ class ScreenshotsHelper { }, `Waiting for window to scroll to ${xPos}, ${yPos}`); } ); - await promise; } async waitForScrollTo(x, y) { @@ -521,6 +513,15 @@ class ScreenshotsHelper { }); } + getOverlaySelectionSizeText(elementId = "testPageElement") { + return ContentTask.spawn(this.browser, [elementId], async () => { + let screenshotsChild = content.windowGlobalChild.getActor( + "ScreenshotsComponent" + ); + return screenshotsChild.overlay.selectionSize.textContent; + }); + } + async clickTestPageElement(elementId = "testPageElement") { let rect = await this.getTestPageElementRect(elementId); let dims = await this.getContentDimensions(); @@ -909,6 +910,21 @@ add_setup(async () => { ); let screenshotBtn = document.getElementById("screenshot-button"); Assert.ok(screenshotBtn, "The screenshots button was added to the nav bar"); + + registerCleanupFunction(async () => { + info(`downloads panel should be visible: ${DownloadsPanel.isPanelShowing}`); + if (DownloadsPanel.isPanelShowing) { + let hiddenPromise = BrowserTestUtils.waitForEvent( + DownloadsPanel.panel, + "popuphidden" + ); + DownloadsPanel.hidePanel(); + await hiddenPromise; + info( + `downloads panel should not be visible: ${DownloadsPanel.isPanelShowing}` + ); + } + }); }); function getContentDevicePixelRatio(browser) { diff --git a/browser/components/search/.eslintrc.js b/browser/components/search/.eslintrc.js index 39079432e7..7224dc6eb7 100644 --- a/browser/components/search/.eslintrc.js +++ b/browser/components/search/.eslintrc.js @@ -5,8 +5,6 @@ "use strict"; module.exports = { - extends: ["plugin:mozilla/require-jsdoc"], - rules: { "mozilla/var-only-at-top-level": "error", }, diff --git a/browser/components/search/DomainToCategoriesMap.worker.mjs b/browser/components/search/DomainToCategoriesMap.worker.mjs deleted file mode 100644 index 07dc52cfb8..0000000000 --- a/browser/components/search/DomainToCategoriesMap.worker.mjs +++ /dev/null @@ -1,101 +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 { PromiseWorker } from "resource://gre/modules/workers/PromiseWorker.mjs"; - -/** - * Boilerplate to connect with the main thread PromiseWorker. - */ -const worker = new PromiseWorker.AbstractWorker(); -worker.dispatch = function (method, args = []) { - return agent[method](...args); -}; -worker.postMessage = function (message, ...transfers) { - self.postMessage(message, ...transfers); -}; -worker.close = function () { - self.close(); -}; - -self.addEventListener("message", msg => worker.handleMessage(msg)); -self.addEventListener("unhandledrejection", function (error) { - throw error.reason; -}); - -/** - * Stores and manages the Domain-to-Categories Map. - */ -class Agent { - /** - * @type {Map<string, Array<number>>} Hashes mapped to categories and values. - */ - #map = new Map(); - - /** - * Converts data from the array directly into a Map. - * - * @param {Array<ArrayBuffer>} fileContents Files - * @returns {boolean} Returns whether the Map contains results. - */ - populateMap(fileContents) { - this.#map.clear(); - - for (let fileContent of fileContents) { - let obj; - try { - obj = JSON.parse(new TextDecoder().decode(fileContent)); - } catch (ex) { - return false; - } - for (let objKey in obj) { - if (Object.hasOwn(obj, objKey)) { - this.#map.set(objKey, obj[objKey]); - } - } - } - return this.#map.size > 0; - } - - /** - * Retrieves scores for the hash from the map. - * - * @param {string} hash Key to look up in the map. - * @returns {Array<number>} - */ - getScores(hash) { - if (this.#map.has(hash)) { - return this.#map.get(hash); - } - return []; - } - - /** - * Empties the internal map. - * - * @returns {boolean} - */ - emptyMap() { - this.#map.clear(); - return true; - } - - /** - * Test only function to allow the map to contain information without - * having to go through Remote Settings. - * - * @param {object} obj The data to directly import into the Map. - * @returns {boolean} Whether the map contains values. - */ - overrideMapForTests(obj) { - this.#map.clear(); - for (let objKey in obj) { - if (Object.hasOwn(obj, objKey)) { - this.#map.set(objKey, obj[objKey]); - } - } - return this.#map.size > 0; - } -} - -const agent = new Agent(); diff --git a/browser/components/search/SearchSERPTelemetry.sys.mjs b/browser/components/search/SearchSERPTelemetry.sys.mjs index fa593be08c..2a9ed88db1 100644 --- a/browser/components/search/SearchSERPTelemetry.sys.mjs +++ b/browser/components/search/SearchSERPTelemetry.sys.mjs @@ -7,12 +7,12 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs", BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "gCryptoHash", () => { @@ -52,11 +52,15 @@ export const SEARCH_TELEMETRY_SHARED = { const impressionIdsWithoutEngagementsSet = new Set(); export const CATEGORIZATION_SETTINGS = { + STORE_SCHEMA: 1, + STORE_FILE: "domain_to_categories.sqlite", + STORE_NAME: "domain_to_categories", MAX_DOMAINS_TO_CATEGORIZE: 10, MINIMUM_SCORE: 0, STARTING_RANK: 2, IDLE_TIMEOUT_SECONDS: 60 * 60, WAKE_TIMEOUT_MS: 60 * 60 * 1000, + PING_SUBMISSION_THRESHOLD: 10, }; ChromeUtils.defineLazyGetter(lazy, "logConsole", () => { @@ -83,15 +87,20 @@ XPCOMUtils.defineLazyPreferenceGetter( false, (aPreference, previousValue, newValue) => { if (newValue) { - SearchSERPDomainToCategoriesMap.init(); - SearchSERPCategorizationEventScheduler.init(); + SearchSERPCategorization.init(); } else { - SearchSERPDomainToCategoriesMap.uninit(); - SearchSERPCategorizationEventScheduler.uninit(); + SearchSERPCategorization.uninit({ deleteMap: true }); } } ); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "activityLimit", + "telemetry.fog.test.activity_limit", + 120 +); + export const SearchSERPTelemetryUtils = { ACTIONS: { CLICKED: "clicked", @@ -380,7 +389,7 @@ class TelemetryHandler { * unit tests can set it to easy to test values. * * @param {Array} providerInfo - * See {@link https://searchfox.org/mozilla-central/search?q=search-telemetry-schema.json} + * See {@link https://searchfox.org/mozilla-central/search?q=search-telemetry-v2-schema.json} * for type information. */ overrideSearchTelemetryForTests(providerInfo) { @@ -1641,7 +1650,10 @@ class ContentHandler { !telemetryState.adImpressionsReported ) { for (let [componentType, data] of info.adImpressions.entries()) { - telemetryState.adsVisible += data.adsVisible; + // Not all ad impressions are sponsored. + if (AD_COMPONENTS.includes(componentType)) { + telemetryState.adsVisible += data.adsVisible; + } lazy.logConsole.debug("Counting ad:", { type: componentType, ...data }); Glean.serp.adImpression.record({ @@ -1772,6 +1784,8 @@ class ContentHandler { let item = this._findItemForBrowser(browser); let telemetryState = item.browserTelemetryStateMap.get(browser); if (lazy.serpEventTelemetryCategorization && telemetryState) { + lazy.logConsole.debug("Ad domains:", Array.from(info.adDomains)); + lazy.logConsole.debug("Non ad domains:", Array.from(info.nonAdDomains)); let result = await SearchSERPCategorization.maybeCategorizeSERP( info.nonAdDomains, info.adDomains, @@ -1789,6 +1803,7 @@ class ContentHandler { partner_code: impressionInfo.partnerCode, provider: impressionInfo.provider, tagged: impressionInfo.tagged, + is_shopping_page: impressionInfo.isShoppingPage, num_ads_clicked: telemetryState.adsClicked, num_ads_visible: telemetryState.adsVisible, }); @@ -1843,6 +1858,22 @@ class ContentHandler { * Categorizes SERPs. */ class SERPCategorizer { + async init() { + if (lazy.serpEventTelemetryCategorization) { + lazy.logConsole.debug("Initialize SERP categorizer."); + await SearchSERPDomainToCategoriesMap.init(); + SearchSERPCategorizationEventScheduler.init(); + SERPCategorizationRecorder.init(); + } + } + + async uninit({ deleteMap = false } = {}) { + lazy.logConsole.debug("Uninit SERP categorizer."); + await SearchSERPDomainToCategoriesMap.uninit(deleteMap); + SearchSERPCategorizationEventScheduler.uninit(); + SERPCategorizationRecorder.uninit(); + } + /** * Categorizes domains extracted from SERPs. Note that we don't process * domains if the domain-to-categories map is empty (if the client couldn't @@ -1999,12 +2030,8 @@ class CategorizationEventScheduler { */ #mostRecentMs = null; - constructor() { - this.init(); - } - init() { - if (!lazy.serpEventTelemetryCategorization || this.#init) { + if (this.#init) { return; } @@ -2114,6 +2141,61 @@ class CategorizationEventScheduler { * Handles reporting SERP categorization telemetry to Glean. */ class CategorizationRecorder { + #init = false; + + // The number of SERP categorizations that have been recorded but not yet + // reported in a Glean ping. + #serpCategorizationsCount = 0; + + // When the user started interacting with the SERP. + #userInteractionStartTime = null; + + async init() { + if (this.#init) { + return; + } + + Services.obs.addObserver(this, "user-interaction-active"); + Services.obs.addObserver(this, "user-interaction-inactive"); + this.#init = true; + this.submitPing("startup"); + Services.obs.notifyObservers(null, "categorization-recorder-init"); + } + + uninit() { + if (this.#init) { + Services.obs.removeObserver(this, "user-interaction-active"); + Services.obs.removeObserver(this, "user-interaction-inactive"); + this.#resetCategorizationRecorderData(); + this.#init = false; + } + } + + observe(subject, topic, _data) { + switch (topic) { + case "user-interaction-active": { + // If the user is already active, we don't want to overwrite the start + // time. + if (this.#userInteractionStartTime == null) { + this.#userInteractionStartTime = Date.now(); + } + break; + } + case "user-interaction-inactive": { + let currentTime = Date.now(); + let activityLimitInMs = lazy.activityLimit * 1000; + if ( + this.#userInteractionStartTime && + currentTime - this.#userInteractionStartTime >= activityLimitInMs + ) { + this.submitPing("inactivity"); + } + this.#userInteractionStartTime = null; + break; + } + } + } + /** * Helper function for recording the SERP categorization event. * @@ -2125,7 +2207,37 @@ class CategorizationRecorder { "Reporting the following categorization result:", resultToReport ); - // TODO: Bug 1868476 - Report result to Glean. + Glean.serp.categorization.record(resultToReport); + + this.#serpCategorizationsCount++; + if ( + this.#serpCategorizationsCount >= + CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD + ) { + this.submitPing("threshold_reached"); + this.#serpCategorizationsCount = 0; + } + } + + submitPing(reason) { + lazy.logConsole.debug("Submitting SERP categorization ping:", reason); + GleanPings.serpCategorization.submit(reason); + } + + /** + * Tests are able to clear telemetry on demand. When that happens, we need to + * ensure we're doing to the same here or else the internal count in tests + * will be inaccurate. + */ + testReset() { + if (Cu.isInAutomation) { + this.#resetCategorizationRecorderData(); + } + } + + #resetCategorizationRecorderData() { + this.#serpCategorizationsCount = 0; + this.#userInteractionStartTime = null; } } @@ -2144,10 +2256,8 @@ class CategorizationRecorder { */ /** - * Maps domain to categories, with its data synced using Remote Settings. The - * data is downloaded from Remote Settings and stored in a map in a worker - * thread to avoid processing the data from the attachments from occupying - * the main thread. + * Maps domain to categories. Data is downloaded from Remote Settings and + * stored inside DomainToCategoriesStore. */ class DomainToCategoriesMap { /** @@ -2195,40 +2305,63 @@ class DomainToCategoriesMap { #downloadRetries = 0; /** - * Whether the mappings are empty. - */ - #empty = true; - - /** - * @type {BasePromiseWorker|null} Worker used to access the raw domain - * to categories map data. + * A reference to the data store. + * + * @type {DomainToCategoriesStore | null} */ - #worker = null; + #store = null; /** * Runs at application startup with startup idle tasks. If the SERP * categorization preference is enabled, it creates a Remote Settings - * client to listen to updates, and populates the map. + * client to listen to updates, and populates the store. */ async init() { - if (!lazy.serpEventTelemetryCategorization || this.#init) { + if (this.#init) { return; } lazy.logConsole.debug("Initializing domain-to-categories map."); - this.#worker = new lazy.BasePromiseWorker( - "resource:///modules/DomainToCategoriesMap.worker.mjs", - { type: "module" } - ); - await this.#setupClientAndMap(); + + // Set early to allow un-init from an initialization. this.#init = true; + + try { + await this.#setupClientAndStore(); + } catch (ex) { + lazy.logConsole.error(ex); + await this.uninit(); + return; + } + + // If we don't have a client and store, it likely means an un-init process + // started during the initialization process. + if (this.#client && this.#store) { + lazy.logConsole.debug("Initialized domain-to-categories map."); + Services.obs.notifyObservers(null, "domain-to-categories-map-init"); + } } - uninit() { + async uninit(shouldDeleteStore) { if (this.#init) { lazy.logConsole.debug("Un-initializing domain-to-categories map."); - this.#clearClientAndWorker(); + this.#clearClient(); this.#cancelAndNullifyTimer(); + + if (this.#store) { + if (shouldDeleteStore) { + try { + await this.#store.dropData(); + } catch (ex) { + lazy.logConsole.error(ex); + } + } + await this.#store.uninit(); + this.#store = null; + } + + lazy.logConsole.debug("Un-initialized domain-to-categories map."); this.#init = false; + Services.obs.notifyObservers(null, "domain-to-categories-map-uninit"); } } @@ -2241,14 +2374,14 @@ class DomainToCategoriesMap { * for the domain is available, return an empty array. */ async get(domain) { - if (this.empty) { + if (!this.#store || this.#store.empty || !this.#store.ready) { return []; } lazy.gCryptoHash.init(lazy.gCryptoHash.SHA256); let bytes = new TextEncoder().encode(domain); lazy.gCryptoHash.update(bytes, domain.length); let hash = lazy.gCryptoHash.finish(true); - let rawValues = await this.#worker.post("getScores", [hash]); + let rawValues = await this.#store.getCategories(hash); if (rawValues?.length) { let output = []; // Transform data into a more readable format. @@ -2275,12 +2408,15 @@ class DomainToCategoriesMap { } /** - * Whether the map is empty of data. + * Whether the store is empty of data. * * @returns {boolean} */ get empty() { - return this.#empty; + if (!this.#store) { + return true; + } + return this.#store.empty; } /** @@ -2290,15 +2426,26 @@ class DomainToCategoriesMap { * @param {object} domainToCategoriesMap * An object where the key is a hashed domain and the value is an array * containing an arbitrary number of DomainCategoryScores. + * @param {number} version + * The version number for the store. */ - async overrideMapForTests(domainToCategoriesMap) { - let hasResults = await this.#worker.post("overrideMapForTests", [ - domainToCategoriesMap, - ]); - this.#empty = !hasResults; + async overrideMapForTests(domainToCategoriesMap, version = 1) { + if (Cu.isInAutomation || Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) { + await this.#store.init(); + await this.#store.dropData(); + await this.#store.insertObject(domainToCategoriesMap, version); + } } - async #setupClientAndMap() { + /** + * Connect with Remote Settings and retrieve the records associated with + * categorization. Then, check if the records match the store version. If + * no records exist, return early. If records exist but the version stored + * on the records differ from the store version, then attempt to + * empty the store and fill it with data from downloaded attachments. Only + * reuse the store if the version in each record matches the store. + */ + async #setupClientAndStore() { if (this.#client && !this.empty) { return; } @@ -2308,11 +2455,33 @@ class DomainToCategoriesMap { this.#onSettingsSync = event => this.#sync(event.data); this.#client.on("sync", this.#onSettingsSync); + this.#store = new DomainToCategoriesStore(); + await this.#store.init(); + let records = await this.#client.get(); - await this.#clearAndPopulateMap(records); + // Even though records don't exist, this is still technically initialized + // since the next sync from Remote Settings will populate the store with + // records. + if (!records.length) { + lazy.logConsole.debug("No records found for domain-to-categories map."); + return; + } + + this.#version = this.#retrieveLatestVersion(records); + let storeVersion = await this.#store.getVersion(); + if (storeVersion == this.#version && !this.#store.empty) { + lazy.logConsole.debug("Reuse existing domain-to-categories map."); + Services.obs.notifyObservers( + null, + "domain-to-categories-map-update-complete" + ); + return; + } + + await this.#clearAndPopulateStore(records); } - #clearClientAndWorker() { + #clearClient() { if (this.#client) { lazy.logConsole.debug("Removing Remote Settings client."); this.#client.off("sync", this.#onSettingsSync); @@ -2320,17 +2489,6 @@ class DomainToCategoriesMap { this.#onSettingsSync = null; this.#downloadRetries = 0; } - - if (!this.#empty) { - lazy.logConsole.debug("Clearing domain-to-categories map."); - this.#empty = true; - this.#version = null; - } - - if (this.#worker) { - this.#worker.terminate(); - this.#worker = null; - } } /** @@ -2377,27 +2535,50 @@ class DomainToCategoriesMap { // again in case there's a new download error. this.#downloadRetries = 0; - this.#clearAndPopulateMap(data?.current); + try { + await this.#clearAndPopulateStore(data?.current); + } catch (ex) { + lazy.logConsole.error("Error populating map: ", ex); + await this.uninit(); + } } /** - * Clear the existing map and populate it with attachments found in the + * Clear the existing store and populate it with attachments found in the * records. If no attachments are found, or no record containing an * attachment contained the latest version, then nothing will change. * * @param {Array<DomainToCategoriesRecord>} records * The records containing attachments. - * + * @throws {Error} + * Will throw if it was not able to drop the store data, or it was unable + * to insert data into the store. */ - async #clearAndPopulateMap(records) { - // Empty map so that if there are errors in the download process, callers - // querying the map won't use information we know is already outdated. - await this.#worker.post("emptyMap"); + async #clearAndPopulateStore(records) { + // If we don't have a handle to a store, it would mean that it was removed + // during an uninitialization process. + if (!this.#store) { + lazy.logConsole.debug( + "Could not populate store because no store was available." + ); + return; + } + + if (!this.#store.ready) { + lazy.logConsole.debug( + "Could not populate store because it was not ready." + ); + return; + } + + // Empty table so that if there are errors in the download process, callers + // querying the map won't use information we know is probably outdated. + await this.#store.dropData(); - this.#empty = true; this.#version = null; this.#cancelAndNullifyTimer(); + // A collection with no records is still a valid init state. if (!records?.length) { lazy.logConsole.debug("No records found for domain-to-categories map."); return; @@ -2418,41 +2599,24 @@ class DomainToCategoriesMap { fileContents.push(result.buffer); } ChromeUtils.addProfilerMarker( - "SearchSERPTelemetry.#clearAndPopulateMap", + "SearchSERPTelemetry.#clearAndPopulateStore", start, "Download attachments." ); - // Attachments should have a version number. this.#version = this.#retrieveLatestVersion(records); - if (!this.#version) { lazy.logConsole.debug("Could not find a version number for any record."); return; } - Services.tm.idleDispatchToMainThread(async () => { - start = Cu.now(); - let hasResults; - try { - hasResults = await this.#worker.post("populateMap", [fileContents]); - } catch (ex) { - console.error(ex); - } + await this.#store.insertFileContents(fileContents, this.#version); - this.#empty = !hasResults; - - ChromeUtils.addProfilerMarker( - "SearchSERPTelemetry.#clearAndPopulateMap", - start, - "Convert contents to JSON." - ); - lazy.logConsole.debug("Updated domain-to-categories map."); - Services.obs.notifyObservers( - null, - "domain-to-categories-map-update-complete" - ); - }); + lazy.logConsole.debug("Finished updating domain-to-categories store."); + Services.obs.notifyObservers( + null, + "domain-to-categories-map-update-complete" + ); } #cancelAndNullifyTimer() { @@ -2466,7 +2630,8 @@ class DomainToCategoriesMap { #createTimerToPopulateMap() { if ( this.#downloadRetries >= - TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxTriesPerSession + TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxTriesPerSession || + !this.#client ) { return; } @@ -2486,7 +2651,12 @@ class DomainToCategoriesMap { async () => { this.#downloadRetries += 1; let records = await this.#client.get(); - this.#clearAndPopulateMap(records); + try { + await this.#clearAndPopulateStore(records); + } catch (ex) { + lazy.logConsole.error("Error populating store: ", ex); + await this.uninit(); + } }, delay, Ci.nsITimer.TYPE_ONE_SHOT @@ -2494,6 +2664,514 @@ class DomainToCategoriesMap { } } +/** + * Handles the storage of data containing domains to categories. + */ +export class DomainToCategoriesStore { + #init = false; + + /** + * The connection to the store. + * + * @type {object | null} + */ + #connection = null; + + /** + * Reference for the shutdown blocker in case we need to remove it before + * shutdown. + * + * @type {Function | null} + */ + #asyncShutdownBlocker = null; + + /** + * Whether the store is empty of data. + * + * @type {boolean} + */ + #empty = true; + + /** + * For a particular subset of errors, we'll attempt to rebuild the database + * from scratch. + */ + #rebuildableErrors = ["NS_ERROR_FILE_CORRUPTED"]; + + /** + * Initializes the store. If the store is initialized it should have cached + * a connection to the store and ensured the store exists. + */ + async init() { + if (this.#init) { + return; + } + lazy.logConsole.debug("Initializing domain-to-categories store."); + + // Attempts to cache a connection to the store. + // If a failure occured, try to re-build the store. + let rebuiltStore = false; + try { + await this.#initConnection(); + } catch (ex1) { + lazy.logConsole.error(`Error initializing a connection: ${ex1}`); + if (this.#rebuildableErrors.includes(ex1.name)) { + try { + await this.#rebuildStore(); + } catch (ex2) { + await this.#closeConnection(); + lazy.logConsole.error(`Could not rebuild store: ${ex2}`); + return; + } + rebuiltStore = true; + } + } + + // If we don't have a connection, bail because the browser could be + // shutting down ASAP, or re-creating the store is impossible. + if (!this.#connection) { + lazy.logConsole.debug( + "Bailing from DomainToCategoriesStore.init because connection doesn't exist." + ); + return; + } + + // If we weren't forced to re-build the store, we only have the connection. + // We want to ensure the store exists so calls to public methods can pass + // without throwing errors due to the absence of the store. + if (!rebuiltStore) { + try { + await this.#initSchema(); + } catch (ex) { + lazy.logConsole.error(`Error trying to create store: ${ex}`); + await this.#closeConnection(); + return; + } + } + + lazy.logConsole.debug("Initialized domain-to-categories store."); + this.#init = true; + } + + async uninit() { + if (this.#init) { + lazy.logConsole.debug("Un-initializing domain-to-categories store."); + await this.#closeConnection(); + this.#asyncShutdownBlocker = null; + lazy.logConsole.debug("Un-initialized domain-to-categories store."); + } + } + + /** + * Whether the store has an open connection to the physical store. + * + * @returns {boolean} + */ + get ready() { + return this.#init; + } + + /** + * Whether the store is devoid of data. + * + * @returns {boolean} + */ + get empty() { + return this.#empty; + } + + /** + * Clears information in the store. If dropping data encountered a failure, + * try to delete the file containing the store and re-create it. + * + * @throws {Error} Will throw if it was unable to clear information from the + * store. + */ + async dropData() { + if (!this.#connection) { + return; + } + let tableExists = await this.#connection.tableExists( + CATEGORIZATION_SETTINGS.STORE_NAME + ); + if (tableExists) { + lazy.logConsole.debug("Drop domain_to_categories."); + // This can fail if the permissions of the store are read-only. + await this.#connection.executeTransaction(async () => { + await this.#connection.execute(`DROP TABLE domain_to_categories`); + const createDomainToCategoriesTable = ` + CREATE TABLE IF NOT EXISTS + domain_to_categories ( + string_id + TEXT PRIMARY KEY NOT NULL, + categories + TEXT + ); + `; + await this.#connection.execute(createDomainToCategoriesTable); + await this.#connection.execute(`DELETE FROM moz_meta`); + await this.#connection.executeCached( + ` + INSERT INTO + moz_meta (key, value) + VALUES + (:key, :value) + ON CONFLICT DO UPDATE SET + value = :value + `, + { key: "version", value: 0 } + ); + }); + + this.#empty = true; + } + } + + /** + * Given file contents, try moving them into the store. If a failure occurs, + * it will attempt to drop existing data to ensure callers aren't accessing + * a partially filled store. + * + * @param {Array<ArrayBuffer>} fileContents + * Contents to convert. + * @param {number} version + * The version for the store. + * @throws {Error} + * Will throw if the insertion failed and dropData was unable to run + * successfully. + */ + async insertFileContents(fileContents, version) { + if (!this.#init || !fileContents?.length || !version) { + return; + } + + try { + await this.#insert(fileContents, version); + } catch (ex) { + lazy.logConsole.error(`Could not insert file contents: ${ex}`); + await this.dropData(); + } + } + + /** + * Convenience function to make it trivial to insert Javascript objects into + * the store. This avoids having to set up the collection in Remote Settings. + * + * @param {object} domainToCategoriesMap + * An object whose keys should be hashed domains with values containing + * an array of integers. + * @param {number} version + * The version for the store. + * @returns {boolean} + * Whether the operation was successful. + */ + async insertObject(domainToCategoriesMap, version) { + if (!Cu.isInAutomation || !this.#init) { + return false; + } + let buffer = new TextEncoder().encode( + JSON.stringify(domainToCategoriesMap) + ).buffer; + await this.insertFileContents([buffer], version); + return true; + } + + /** + * Retrieves domains mapped to the key. + * + * @param {string} key + * The value to lookup in the store. + * @returns {Array<number>} + * An array of numbers corresponding to the category and score. If the key + * does not exist in the store or the store is having issues retrieving the + * value, returns an empty array. + */ + async getCategories(key) { + if (!this.#init) { + return []; + } + + let rows; + try { + rows = await this.#connection.executeCached( + ` + SELECT + categories + FROM + domain_to_categories + WHERE + string_id = :key + `, + { + key, + } + ); + } catch (ex) { + lazy.logConsole.error(`Could not retrieve from the store: ${ex}`); + return []; + } + + if (!rows.length) { + return []; + } + return JSON.parse(rows[0].getResultByName("categories")) ?? []; + } + + /** + * Retrieves the version number of the store. + * + * @returns {number} + * The version number. Returns 0 if the version was never set or if there + * was an issue accessing the version number. + */ + async getVersion() { + if (this.#connection) { + let rows; + try { + rows = await this.#connection.executeCached( + ` + SELECT + value + FROM + moz_meta + WHERE + key = "version" + ` + ); + } catch (ex) { + lazy.logConsole.error(`Could not retrieve version of the store: ${ex}`); + return 0; + } + if (rows.length) { + return parseInt(rows[0].getResultByName("value")) ?? 0; + } + } + return 0; + } + + /** + * Test only function allowing tests to delete the store. + */ + async testDelete() { + if (Cu.isInAutomation) { + await this.#closeConnection(); + await this.#delete(); + } + } + + /** + * If a connection is available, close it and remove shutdown blockers. + */ + async #closeConnection() { + this.#init = false; + this.#empty = true; + if (this.#asyncShutdownBlocker) { + lazy.Sqlite.shutdown.removeBlocker(this.#asyncShutdownBlocker); + this.#asyncShutdownBlocker = null; + } + + if (this.#connection) { + lazy.logConsole.debug("Closing connection."); + // An error could occur while closing the connection. We suppress the + // error since it is not a critical part of the browser. + try { + await this.#connection.close(); + } catch (ex) { + lazy.logConsole.error(ex); + } + this.#connection = null; + } + } + + /** + * Initialize the schema for the store. + * + * @throws {Error} + * Will throw if a permissions error prevents creating the store. + */ + async #initSchema() { + if (!this.#connection) { + return; + } + lazy.logConsole.debug("Create store."); + // Creation can fail if the store is read only. + await this.#connection.executeTransaction(async () => { + // Let outer try block handle the exception. + const createDomainToCategoriesTable = ` + CREATE TABLE IF NOT EXISTS + domain_to_categories ( + string_id + TEXT PRIMARY KEY NOT NULL, + categories + TEXT + ) WITHOUT ROWID; + `; + await this.#connection.execute(createDomainToCategoriesTable); + const createMetaTable = ` + CREATE TABLE IF NOT EXISTS + moz_meta ( + key + TEXT PRIMARY KEY NOT NULL, + value + INTEGER + ) WITHOUT ROWID; + `; + await this.#connection.execute(createMetaTable); + await this.#connection.setSchemaVersion( + CATEGORIZATION_SETTINGS.STORE_SCHEMA + ); + }); + + let rows = await this.#connection.executeCached( + "SELECT count(*) = 0 FROM domain_to_categories" + ); + this.#empty = !!rows[0].getResultByIndex(0); + } + + /** + * Attempt to delete the store. + * + * @throws {Error} + * Will throw if the permissions for the file prevent its deletion. + */ + async #delete() { + lazy.logConsole.debug("Attempt to delete the store."); + try { + await IOUtils.remove( + PathUtils.join( + PathUtils.profileDir, + CATEGORIZATION_SETTINGS.STORE_FILE + ), + { ignoreAbsent: true } + ); + } catch (ex) { + lazy.logConsole.error(ex); + } + this.#empty = true; + lazy.logConsole.debug("Store was deleted."); + } + + /** + * Tries to establish a connection to the store. + * + * @throws {Error} + * Will throw if there was an issue establishing a connection or adding + * adding a shutdown blocker. + */ + async #initConnection() { + if (this.#connection) { + return; + } + + // This could fail if the store is corrupted. + this.#connection = await lazy.Sqlite.openConnection({ + path: PathUtils.join( + PathUtils.profileDir, + CATEGORIZATION_SETTINGS.STORE_FILE + ), + }); + + await this.#connection.execute("PRAGMA journal_mode = TRUNCATE"); + + this.#asyncShutdownBlocker = async () => { + await this.#connection.close(); + this.#connection = null; + }; + + // This could fail if we're adding it during shutdown. In this case, + // don't throw but close the connection. + try { + lazy.Sqlite.shutdown.addBlocker( + "SearchSERPTelemetry:DomainToCategoriesSqlite closing", + this.#asyncShutdownBlocker + ); + } catch (ex) { + lazy.logConsole.error(ex); + await this.#closeConnection(); + } + } + + /** + * Inserts into the store. + * + * @param {Array<ArrayBuffer>} fileContents + * The data that should be converted and inserted into the store. + * @param {number} version + * The version number that should be inserted into the store. + * @throws {Error} + * Will throw if a connection is not present, if the store is not + * able to be updated (permissions error, corrupted file), or there is + * something wrong with the file contents. + */ + async #insert(fileContents, version) { + let start = Cu.now(); + await this.#connection.executeTransaction(async () => { + lazy.logConsole.debug("Insert into domain_to_categories table."); + for (let fileContent of fileContents) { + await this.#connection.executeCached( + ` + INSERT INTO + domain_to_categories (string_id, categories) + SELECT + json_each.key AS string_id, + json_each.value AS categories + FROM + json_each(json(:obj)) + `, + { + obj: new TextDecoder().decode(fileContent), + } + ); + } + // Once the insertions have successfully completed, update the version. + await this.#connection.executeCached( + ` + INSERT INTO + moz_meta (key, value) + VALUES + (:key, :value) + ON CONFLICT DO UPDATE SET + value = :value + `, + { key: "version", value: version } + ); + }); + ChromeUtils.addProfilerMarker( + "DomainToCategoriesSqlite.#insert", + start, + "Move file contents into table." + ); + + if (fileContents?.length) { + this.#empty = false; + } + } + + /** + * Deletes and re-build's the store. Used in cases where we encounter a + * failure and we want to try fixing the error by starting with an + * entirely fresh store. + * + * @throws {Error} + * Will throw if a connection could not be established, if it was + * unable to delete the store, or it was unable to build a new store. + */ + async #rebuildStore() { + lazy.logConsole.debug("Try rebuilding store."); + // Step 1. Close all connections. + await this.#closeConnection(); + + // Step 2. Delete the existing store. + await this.#delete(); + + // Step 3. Re-establish the connection. + await this.#initConnection(); + + // Step 4. If a connection exists, try creating the store. + await this.#initSchema(); + } +} + function randomInteger(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } diff --git a/browser/components/search/metrics.yaml b/browser/components/search/metrics.yaml index 12fd44a0e2..c7636b9d04 100644 --- a/browser/components/search/metrics.yaml +++ b/browser/components/search/metrics.yaml @@ -331,6 +331,110 @@ serp: - fx-search-telemetry@mozilla.com expires: never + categorization: + type: event + description: > + A high-level categorization of a SERP (a best guess as to its topic), + using buckets such as "sports" or "travel". + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1868476 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1869064 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1887686 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1868476 + data_sensitivity: + - stored_content + notification_emails: + - fx-search-telemetry@mozilla.com + - rev-data@mozilla.com + expires: never + extra_keys: + sponsored_category: + description: > + An index corresponding to a broad category for the SERP, derived from + sponsored domains. + type: quantity + sponsored_num_domains: + description: > + The total number of sponsored domains used in the categorization + process for the SERP. + type: quantity + sponsored_num_unknown: + description: > + The count of sponsored domains extracted from the SERP that are not + found in the domain-to-categories mapping. + type: quantity + sponsored_num_inconclusive: + description: > + The count of sponsored domains extracted from the SERP that are found + in the domain-to-categories mapping but are deemed inconclusive. + type: quantity + organic_category: + description: > + An index corresponding to a broad category for the SERP, derived from + organic domains. + type: quantity + organic_num_domains: + description: > + The total number of organic domains used in the categorization + process for the SERP. + type: quantity + organic_num_unknown: + description: > + The count of organic domains extracted from the SERP that are not + found in the domain-to-categories mapping. + type: quantity + organic_num_inconclusive: + description: > + The count of organic domains extracted from the SERP that are found + in the domain-to-categories mapping but are deemed inconclusive. + type: quantity + region: + description: > + A two-letter country code indicating where the SERP was loaded. + type: string + channel: + description: > + The type of update channel, for example: “nightly”, “beta”, “release”. + type: string + provider: + description: > + The name of the provider. + type: string + tagged: + description: > + Whether the search is tagged (true) or organic (false). + type: boolean + partner_code: + description: > + Any partner_code parsing in the URL or an empty string if not + available. + type: string + app_version: + description: > + The Firefox major version used, for example: 126. + type: quantity + mappings_version: + description: > + Version number for the Remote Settings attachments used to generate + the domain-to-categories map used in the SERP categorization process. + type: quantity + is_shopping_page: + description: > + Indicates if the page is a shopping page. + type: boolean + num_ads_visible: + description: > + Number of ads visible on the page at the time of categorizing the + page. + type: quantity + num_ads_clicked: + description: > + Number of ads clicked on the page. + type: quantity + send_in_pings: + - serp-categorization + search_with: reporting_url: type: url diff --git a/browser/components/search/moz.build b/browser/components/search/moz.build index 0289f32979..ff49a259ed 100644 --- a/browser/components/search/moz.build +++ b/browser/components/search/moz.build @@ -6,7 +6,6 @@ EXTRA_JS_MODULES += [ "BrowserSearchTelemetry.sys.mjs", - "DomainToCategoriesMap.worker.mjs", "SearchOneOffs.sys.mjs", "SearchSERPTelemetry.sys.mjs", "SearchUIUtils.sys.mjs", @@ -18,7 +17,10 @@ BROWSER_CHROME_MANIFESTS += [ "test/browser/telemetry/browser.toml", ] -MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"] +MARIONETTE_MANIFESTS += [ + "test/marionette/manifest.toml", + "test/marionette/telemetry/manifest.toml", +] XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"] diff --git a/browser/components/search/schema/search-telemetry-schema.json b/browser/components/search/schema/search-telemetry-v2-schema.json index 50b6e124fc..50b6e124fc 100644 --- a/browser/components/search/schema/search-telemetry-schema.json +++ b/browser/components/search/schema/search-telemetry-v2-schema.json diff --git a/browser/components/search/schema/search-telemetry-ui-schema.json b/browser/components/search/schema/search-telemetry-v2-ui-schema.json index 781da5a626..749063db72 100644 --- a/browser/components/search/schema/search-telemetry-ui-schema.json +++ b/browser/components/search/schema/search-telemetry-v2-ui-schema.json @@ -11,10 +11,12 @@ "organicCodes", "followOnParamNames", "followOnCookies", + "ignoreLinkRegexps", "extraAdServersRegexps", "adServerAttributes", "components", "nonAdsLinkRegexps", + "nonAdsLinkQueryParamNames", "shoppingTab", "domainExtraction", "isSPA", diff --git a/browser/components/search/test/browser/telemetry/browser.toml b/browser/components/search/test/browser/telemetry/browser.toml index 660fc4eae2..5e42a9187d 100644 --- a/browser/components/search/test/browser/telemetry/browser.toml +++ b/browser/components/search/test/browser/telemetry/browser.toml @@ -50,6 +50,15 @@ support-files = ["searchTelemetryDomainCategorizationReporting.html"] ["browser_search_telemetry_domain_categorization_extraction.js"] support-files = ["searchTelemetryDomainExtraction.html"] +["browser_search_telemetry_domain_categorization_no_sponsored_values.js"] +support-files = ["searchTelemetryDomainCategorizationReportingWithoutAds.html"] + +["browser_search_telemetry_domain_categorization_ping_submission.js"] +support-files = [ + "searchTelemetryDomainCategorizationReporting.html", + "searchTelemetryDomainExtraction.html", +] + ["browser_search_telemetry_domain_categorization_region.js"] support-files = ["searchTelemetryDomainCategorizationReporting.html"] @@ -103,13 +112,6 @@ support-files = [ "searchTelemetryAd_searchbox_with_content.html^headers^", ] -["browser_search_telemetry_engagement_non_ad.js"] -support-files = [ - "searchTelemetryAd_searchbox_with_content.html", - "searchTelemetryAd_searchbox_with_content.html^headers^", - "serp.css", -] - ["browser_search_telemetry_engagement_nonAdsLinkQueryParamNames.js"] support-files = [ "searchTelemetryAd_searchbox_with_redirecting_links.html", @@ -118,6 +120,13 @@ support-files = [ "serp.css", ] +["browser_search_telemetry_engagement_non_ad.js"] +support-files = [ + "searchTelemetryAd_searchbox_with_content.html", + "searchTelemetryAd_searchbox_with_content.html^headers^", + "serp.css", +] + ["browser_search_telemetry_engagement_query_params.js"] support-files = [ "searchTelemetryAd_components_query_parameters.html", diff --git a/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js b/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js index e73a9601d4..8e9db64fae 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js +++ b/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js @@ -74,11 +74,14 @@ add_setup(async function () { let oldCanRecord = Services.telemetry.canRecordExtended; Services.telemetry.canRecordExtended = true; - await insertRecordIntoCollectionAndSync(); // If the categorization preference is enabled, we should also wait for the // sync event to update the domain to categories map. if (lazy.serpEventsCategorizationEnabled) { - await waitForDomainToCategoriesUpdate(); + let promise = waitForDomainToCategoriesUpdate(); + await insertRecordIntoCollectionAndSync(); + await promise; + } else { + await insertRecordIntoCollectionAndSync(); } registerCleanupFunction(async () => { @@ -99,6 +102,11 @@ add_task(async function test_enable_experiment_when_pref_is_not_enabled() { // the default branch, and not overwrite the user branch. prefBranch.setBoolPref(TELEMETRY_PREF, false); + // If it was true, we should wait until the map is fully un-inited. + if (originalPrefValue) { + await waitForDomainToCategoriesUninit(); + } + Assert.equal( lazy.serpEventsCategorizationEnabled, false, @@ -152,6 +160,7 @@ add_task(async function test_enable_experiment_when_pref_is_not_enabled() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }, @@ -160,6 +169,7 @@ add_task(async function test_enable_experiment_when_pref_is_not_enabled() { info("End experiment."); await doExperimentCleanup(); + await waitForDomainToCategoriesUninit(); Assert.equal( lazy.serpEventsCategorizationEnabled, @@ -179,6 +189,7 @@ add_task(async function test_enable_experiment_when_pref_is_not_enabled() { await new Promise(resolve => setTimeout(resolve, 1500)); BrowserTestUtils.removeTab(tab); + // We should not record telemetry if the experiment is un-enrolled. assertCategorizationValues([]); // Clean up. diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js index 246caf6f47..daccbf0c93 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js @@ -71,6 +71,16 @@ add_setup(async function () { await promise; registerCleanupFunction(async () => { + // Manually unload the pref so that we can check if we should wait for the + // the categories map to be un-initialized. + await SpecialPowers.popPrefEnv(); + if ( + !Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + await waitForDomainToCategoriesUninit(); + } SearchSERPTelemetry.overrideSearchTelemetryForTests(); resetTelemetry(); }); @@ -103,6 +113,7 @@ add_task(async function test_load_serp_and_categorize() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }, @@ -143,6 +154,7 @@ add_task(async function test_load_serp_and_categorize_and_click_organic() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }, @@ -181,6 +193,7 @@ add_task(async function test_load_serp_and_categorize_and_click_sponsored() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "1", num_ads_visible: "2", }, diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js index b8dd85da97..9bd215f697 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js @@ -82,11 +82,20 @@ add_setup(async function () { await db.clear(); - // Set the state of the pref to false so that tests toggle the preference, - // triggering the map to be updated. - await SpecialPowers.pushPrefEnv({ - set: [["browser.search.serpEventTelemetryCategorization.enabled", false]], - }); + // If the pref is by default on, disable it as the following tests toggle + // the preference to check what happens when the preference is off and the + // preference is turned on. + if ( + Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + let promise = waitForDomainToCategoriesUninit(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.serpEventTelemetryCategorization.enabled", false]], + }); + await promise; + } let defaultDownloadSettings = { ...TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS, @@ -104,6 +113,16 @@ add_setup(async function () { TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxAdjust = 0; registerCleanupFunction(async () => { + // Manually unload the pref so that we can check if we should wait for the + // the categories map to be initialized. + await SpecialPowers.popPrefEnv(); + if ( + Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + await waitForDomainToCategoriesInit(); + } SearchSERPTelemetry.overrideSearchTelemetryForTests(); resetTelemetry(); TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS = { @@ -159,6 +178,7 @@ add_task(async function test_download_after_failure() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_visible: "2", num_ads_clicked: "0", }, @@ -166,6 +186,7 @@ add_task(async function test_download_after_failure() { // Clean up. await SpecialPowers.popPrefEnv(); + await waitForDomainToCategoriesUninit(); await resetCategorizationCollection(record); }); @@ -214,6 +235,7 @@ add_task(async function test_download_after_multiple_failures() { // Clean up. await SpecialPowers.popPrefEnv(); + await waitForDomainToCategoriesUninit(); await resetCategorizationCollection(record); }); @@ -245,6 +267,7 @@ add_task(async function test_cancel_download_timer() { }); await SpecialPowers.popPrefEnv(); await observeCancel; + await waitForDomainToCategoriesUninit(); // To ensure we don't attempt another download, wait a bit over how long the // the download error should take. @@ -263,7 +286,6 @@ add_task(async function test_cancel_download_timer() { Assert.ok(SearchSERPDomainToCategoriesMap.empty, "Map is empty"); // Clean up. - await SpecialPowers.popPrefEnv(); await resetCategorizationCollection(record); }); @@ -310,6 +332,7 @@ add_task(async function test_download_adjust() { // Clean up. await SpecialPowers.popPrefEnv(); + await waitForDomainToCategoriesUninit(); await resetCategorizationCollection(record); TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.base = TIMEOUT_IN_MS; TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.minAdjust = 0; diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_extraction.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_extraction.js index e653be6c48..2d13b147a2 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_extraction.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_extraction.js @@ -362,19 +362,57 @@ const TESTS = [ ], expectedDomains: ["organic.com"], }, + { + title: "Bing organic result with a path in the URL.", + extractorInfos: [ + { + selectors: "#test26 #b_results .b_algo .b_attribution cite", + method: "textContent", + }, + ], + expectedDomains: ["organic.com"], + }, + { + title: "Bing organic result with a path and query param in the URL.", + extractorInfos: [ + { + selectors: "#test27 #b_results .b_algo .b_attribution cite", + method: "textContent", + }, + ], + expectedDomains: ["organic.com"], + }, + { + title: + "Bing organic result with a path in the URL, but protocol appears in separate HTML element.", + extractorInfos: [ + { + selectors: "#test28 #b_results .b_algo .b_attribution cite", + method: "textContent", + }, + ], + expectedDomains: ["wikipedia.org"], + }, ]; add_setup(async function () { await SpecialPowers.pushPrefEnv({ - set: [ - ["browser.search.serpEventTelemetry.enabled", true], - ["browser.search.serpEventTelemetryCategorization.enabled", true], - ], + set: [["browser.search.serpEventTelemetryCategorization.enabled", true]], }); await SearchSERPTelemetry.init(); registerCleanupFunction(async () => { + // Manually unload the pref so that we can check if we should wait for the + // the categories map to be un-initialized. + await SpecialPowers.popPrefEnv(); + if ( + !Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + await waitForDomainToCategoriesUninit(); + } resetTelemetry(); }); }); diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_no_sponsored_values.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_no_sponsored_values.js new file mode 100644 index 0000000000..2375cad82a --- /dev/null +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_no_sponsored_values.js @@ -0,0 +1,141 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * Checks reporting of pages without ads is accurate. + */ + +ChromeUtils.defineESModuleGetters(this, { + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", +}); + +const TEST_PROVIDER_INFO = [ + { + telemetryId: "example", + searchPageRegexp: + /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetry/, + queryParamNames: ["s"], + codeParamName: "abc", + taggedCodes: ["ff"], + adServerAttributes: ["mozAttr"], + nonAdsLinkRegexps: [], + extraAdServersRegexps: [/^https:\/\/example\.com\/ad/], + shoppingTab: { + selector: "#shopping", + }, + // The search telemetry entry responsible for targeting the specific results. + domainExtraction: { + ads: [], + nonAds: [ + { + selectors: "#results .organic a", + method: "href", + }, + ], + }, + components: [ + { + type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + default: true, + }, + ], + }, +]; + +add_setup(async function () { + SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); + await waitForIdle(); + + let promise = waitForDomainToCategoriesUpdate(); + await insertRecordIntoCollectionAndSync(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.serpEventTelemetryCategorization.enabled", true]], + }); + await promise; + + registerCleanupFunction(async () => { + // Manually unload the pref so that we can check if we should wait for the + // the categories map to be un-initialized. + await SpecialPowers.popPrefEnv(); + if ( + !Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + await waitForDomainToCategoriesUninit(); + } + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + resetTelemetry(); + }); +}); + +add_task( + async function test_load_serp_without_sponsored_links_and_categorize() { + resetTelemetry(); + + let url = getSERPUrl( + "searchTelemetryDomainCategorizationReportingWithoutAds.html" + ); + info("Load a SERP with organic and ad components that are non-sponsored."); + let promise = waitForPageWithCategorizedDomains(); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await promise; + + info("Assert there is a non-sponsored component on the page."); + assertSERPTelemetry([ + { + impression: { + shopping_tab_displayed: "true", + provider: "example", + source: "unknown", + tagged: "true", + is_private: "false", + is_shopping_page: "false", + partner_code: "ff", + }, + adImpressions: [ + { + component: SearchSERPTelemetryUtils.COMPONENTS.SHOPPING_TAB, + ads_loaded: "1", + ads_visible: "1", + ads_hidden: "0", + }, + ], + }, + ]); + + info("Click on the non-sponsored component."); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#shopping", + {}, + tab.linkedBrowser + ); + + await BrowserTestUtils.removeTab(tab); + info("Assert no ads were visible or clicked on."); + assertCategorizationValues([ + { + organic_category: "3", + organic_num_domains: "1", + organic_num_inconclusive: "0", + organic_num_unknown: "0", + sponsored_category: "0", + sponsored_num_domains: "0", + sponsored_num_inconclusive: "0", + sponsored_num_unknown: "0", + mappings_version: "1", + app_version: APP_MAJOR_VERSION, + channel: CHANNEL, + region: REGION, + partner_code: "ff", + provider: "example", + tagged: "true", + is_shopping_page: "false", + num_ads_clicked: "0", + num_ads_visible: "0", + }, + ]); + } +); diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js new file mode 100644 index 0000000000..0196483b8c --- /dev/null +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js @@ -0,0 +1,302 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* + * This test ensures we are correctly submitting the custom ping for SERP + * categorization. (Please see the search component's Marionette tests for + * a test of the ping's submission upon startup.) + */ + +ChromeUtils.defineESModuleGetters(this, { + CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPCategorizationRecorder: "resource:///modules/SearchSERPTelemetry.sys.mjs", + TELEMETRY_CATEGORIZATION_KEY: + "resource:///modules/SearchSERPTelemetry.sys.mjs", +}); + +const TEST_PROVIDER_INFO = [ + { + telemetryId: "example", + searchPageRegexp: + /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetry/, + queryParamNames: ["s"], + codeParamName: "abc", + taggedCodes: ["ff"], + adServerAttributes: ["mozAttr"], + nonAdsLinkRegexps: [/^https:\/\/example.com/], + extraAdServersRegexps: [/^https:\/\/example\.com\/ad/], + // The search telemetry entry responsible for targeting the specific results. + domainExtraction: { + ads: [ + { + selectors: "[data-ad-domain]", + method: "data-attribute", + options: { + dataAttributeKey: "adDomain", + }, + }, + { + selectors: ".ad", + method: "href", + options: { + queryParamKey: "ad_domain", + }, + }, + ], + nonAds: [ + { + selectors: "#results .organic a", + method: "href", + }, + ], + }, + components: [ + { + type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + default: true, + }, + ], + }, +]; + +const client = RemoteSettings(TELEMETRY_CATEGORIZATION_KEY); +const db = client.db; + +function sleep(ms) { + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + return new Promise(resolve => setTimeout(resolve, ms)); +} + +add_setup(async function () { + SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); + await waitForIdle(); + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + await db.clear(); + + let promise = waitForDomainToCategoriesUpdate(); + await insertRecordIntoCollectionAndSync(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.serpEventTelemetryCategorization.enabled", true]], + }); + await promise; + + registerCleanupFunction(async () => { + // Manually unload the pref so that we can check if we should wait for the + // the categories map to be un-initialized. + await SpecialPowers.popPrefEnv(); + if ( + !Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + await waitForDomainToCategoriesUninit(); + } + + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + Services.telemetry.canRecordExtended = oldCanRecord; + resetTelemetry(); + }); +}); + +add_task(async function test_threshold_reached() { + resetTelemetry(); + + let oldThreshold = CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD; + // For testing, it's fine to categorize fewer SERPs before sending the ping. + CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD = 2; + SERPCategorizationRecorder.uninit(); + SERPCategorizationRecorder.init(); + + Assert.equal( + null, + Glean.serp.categorization.testGetValue(), + "Should not have recorded any metrics yet." + ); + + let submitted = false; + GleanPings.serpCategorization.testBeforeNextSubmit(reason => { + submitted = true; + Assert.equal( + "threshold_reached", + reason, + "Ping submission reason should be 'threshold_reached'." + ); + }); + + // Categorize first SERP, which results in one organic and one sponsored + // reporting. + let url = getSERPUrl("searchTelemetryDomainCategorizationReporting.html"); + info("Load a sample SERP with organic and sponsored results."); + let promise = waitForPageWithCategorizedDomains(); + let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await promise; + + Assert.equal( + false, + submitted, + "Ping should not be submitted before threshold is reached." + ); + + // Categorize second SERP, which results in one organic and one sponsored + // reporting. + url = getSERPUrl("searchTelemetryDomainExtraction.html"); + info("Load a sample SERP with organic and sponsored results."); + promise = waitForPageWithCategorizedDomains(); + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await promise; + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + + Assert.equal( + true, + submitted, + "Ping should be submitted once threshold is reached." + ); + + CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD = oldThreshold; +}); + +add_task(async function test_quick_activity_to_inactivity_alternation() { + resetTelemetry(); + + Assert.equal( + null, + Glean.serp.categorization.testGetValue(), + "Should not have recorded any metrics yet." + ); + + let submitted = false; + GleanPings.serpCategorization.testBeforeNextSubmit(() => { + submitted = true; + }); + + let url = getSERPUrl("searchTelemetryDomainCategorizationReporting.html"); + info("Load a sample SERP with organic and sponsored results."); + let promise = waitForPageWithCategorizedDomains(); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await promise; + + let activityDetectedPromise = TestUtils.topicObserved( + "user-interaction-active" + ); + // Simulate ~2.5 seconds of activity. + for (let i = 0; i < 25; i++) { + EventUtils.synthesizeKey("KEY_Enter"); + await sleep(100); + } + await activityDetectedPromise; + + let inactivityDetectedPromise = TestUtils.topicObserved( + "user-interaction-inactive" + ); + await inactivityDetectedPromise; + + Assert.equal( + false, + submitted, + "Ping should not be submitted after a quick alternation from activity to inactivity." + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_submit_after_activity_then_inactivity() { + resetTelemetry(); + let oldActivityLimit = Services.prefs.getIntPref( + "telemetry.fog.test.activity_limit" + ); + Services.prefs.setIntPref("telemetry.fog.test.activity_limit", 2); + + Assert.equal( + null, + Glean.serp.categorization.testGetValue(), + "Should not have recorded any metrics yet." + ); + + let submitted = false; + GleanPings.serpCategorization.testBeforeNextSubmit(reason => { + submitted = true; + Assert.equal( + "inactivity", + reason, + "Ping submission reason should be 'inactivity'." + ); + }); + + let url = getSERPUrl("searchTelemetryDomainCategorizationReporting.html"); + info("Load a sample SERP with organic and sponsored results."); + let promise = waitForPageWithCategorizedDomains(); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + await promise; + + let activityDetectedPromise = TestUtils.topicObserved( + "user-interaction-active" + ); + // Simulate ~2.5 seconds of activity. + for (let i = 0; i < 25; i++) { + EventUtils.synthesizeKey("KEY_Enter"); + await sleep(100); + } + await activityDetectedPromise; + + let inactivityDetectedPromise = TestUtils.topicObserved( + "user-interaction-inactive" + ); + await inactivityDetectedPromise; + + Assert.equal( + true, + submitted, + "Ping should be submitted after 2+ seconds of activity, followed by inactivity." + ); + + BrowserTestUtils.removeTab(tab); + Services.prefs.setIntPref( + "telemetry.fog.test.activity_limit", + oldActivityLimit + ); +}); + +add_task(async function test_no_observers_added_if_pref_is_off() { + resetTelemetry(); + + let prefOnActiveObserverCount = Array.from( + Services.obs.enumerateObservers("user-interaction-active") + ).length; + let prefOnInactiveObserverCount = Array.from( + Services.obs.enumerateObservers("user-interaction-inactive") + ).length; + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.serpEventTelemetryCategorization.enabled", false]], + }); + await waitForDomainToCategoriesUninit(); + + let prefOffActiveObserverCount = Array.from( + Services.obs.enumerateObservers("user-interaction-active") + ).length; + let prefOffInactiveObserverCount = Array.from( + Services.obs.enumerateObservers("user-interaction-inactive") + ).length; + + Assert.equal( + prefOnActiveObserverCount - prefOffActiveObserverCount, + 1, + "There should be one fewer active observer when the pref is off." + ); + Assert.equal( + prefOnInactiveObserverCount - prefOffInactiveObserverCount, + 1, + "There should be one fewer inactive observer when the pref is off." + ); + + await SpecialPowers.popPrefEnv(); + await waitForDomainToCategoriesInit(); +}); diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_region.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_region.js index 4c47b0b14a..7dbf605396 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_region.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_region.js @@ -78,6 +78,17 @@ add_setup(async function () { Assert.equal(Region.home, "DE", "Region"); registerCleanupFunction(async () => { + // Manually unload the pref so that we can check if we should wait for the + // the categories map to be un-initialized. + await SpecialPowers.popPrefEnv(); + if ( + !Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + await waitForDomainToCategoriesUninit(); + } + Region._setHomeRegion(originalHomeRegion); Region._setCurrentRegion(originalCurrentRegion); @@ -113,6 +124,7 @@ add_task(async function test_categorize_page_with_different_region() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }, diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js index 973f17b760..3c439844d7 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js @@ -19,6 +19,7 @@ const TEST_PROVIDER_INFO = [ queryParamNames: ["s"], codeParamName: "abc", taggedCodes: ["ff"], + organicCodes: [], adServerAttributes: ["mozAttr"], nonAdsLinkRegexps: [], extraAdServersRegexps: [ @@ -56,6 +57,9 @@ const TEST_PROVIDER_INFO = [ default: true, }, ], + shoppingTab: { + regexp: "&page=shop", + }, }, ]; @@ -69,6 +73,10 @@ add_setup(async function () { SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO); await waitForIdle(); + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + let { record, attachment } = await insertRecordIntoCollection(); categorizationRecord = record; categorizationAttachment = attachment; @@ -82,7 +90,18 @@ add_setup(async function () { await promise; registerCleanupFunction(async () => { + // Manually unload the pref so that we can check if we should wait for the + // the categories map to be un-initialized. + await SpecialPowers.popPrefEnv(); + if ( + !Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + await waitForDomainToCategoriesUninit(); + } SearchSERPTelemetry.overrideSearchTelemetryForTests(); + Services.telemetry.canRecordExtended = oldCanRecord; resetTelemetry(); await db.clear(); }); @@ -115,6 +134,7 @@ add_task(async function test_categorization_reporting() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }, @@ -147,6 +167,7 @@ add_task(async function test_no_reporting_if_download_failure() { await promise; await BrowserTestUtils.removeTab(tab); + // We should not record telemetry if attachments weren't downloaded. assertCategorizationValues([]); // Re-insert the attachment for other tests. @@ -177,6 +198,7 @@ add_task(async function test_no_reporting_if_no_records() { await promise; await BrowserTestUtils.removeTab(tab); + // We should not record telemetry if there are no records. assertCategorizationValues([]); }); @@ -218,8 +240,46 @@ add_task(async function test_reporting_limited_to_10_domains_of_each_kind() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "12", }, ]); }); + +add_task(async function test_categorization_reporting_for_shopping_page() { + resetTelemetry(); + + let url = getSERPUrl("searchTelemetryDomainCategorizationReporting.html"); + let shoppingUrl = new URL(url); + shoppingUrl.searchParams.set("page", "shop"); + shoppingUrl = shoppingUrl.toString(); + info("Load a sample shopping page SERP with organic and sponsored results."); + let promise = waitForPageWithCategorizedDomains(); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, shoppingUrl); + await promise; + + await BrowserTestUtils.removeTab(tab); + assertCategorizationValues([ + { + organic_category: "3", + organic_num_domains: "1", + organic_num_inconclusive: "0", + organic_num_unknown: "0", + sponsored_category: "4", + sponsored_num_domains: "2", + sponsored_num_inconclusive: "0", + sponsored_num_unknown: "0", + mappings_version: "1", + app_version: APP_MAJOR_VERSION, + channel: CHANNEL, + region: REGION, + partner_code: "ff", + provider: "example", + tagged: "true", + is_shopping_page: "true", + num_ads_clicked: "0", + num_ads_visible: "2", + }, + ]); +}); diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js index 9d3ac2c931..0e2d1c07fd 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js @@ -87,9 +87,20 @@ add_setup(async function () { await promise; registerCleanupFunction(async () => { - // The scheduler uses the mock idle service. - SearchSERPCategorizationEventScheduler.uninit(); - SearchSERPCategorizationEventScheduler.init(); + // Manually unload the pref so that we can check if we should wait for the + // the categories map to be un-initialized. + await SpecialPowers.popPrefEnv(); + if ( + !Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + await waitForDomainToCategoriesUninit(); + } else { + // The scheduler uses the mock idle service. + SearchSERPCategorizationEventScheduler.uninit(); + SearchSERPCategorizationEventScheduler.init(); + } SearchSERPTelemetry.overrideSearchTelemetryForTests(); resetTelemetry(); }); @@ -126,6 +137,7 @@ add_task(async function test_categorize_serp_and_wait() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }, @@ -170,6 +182,7 @@ add_task(async function test_categorize_serp_open_multiple_tabs() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }); @@ -223,6 +236,7 @@ add_task(async function test_categorize_serp_close_tab_and_wait() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }, @@ -276,6 +290,7 @@ add_task(async function test_categorize_serp_open_ad_and_wait() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "1", num_ads_visible: "2", }, diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js index c73e224eae..43c520a8d0 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js @@ -92,9 +92,20 @@ add_setup(async function () { await promise; registerCleanupFunction(async () => { - // The scheduler uses the mock idle service. - SearchSERPCategorizationEventScheduler.uninit(); - SearchSERPCategorizationEventScheduler.init(); + // Manually unload the pref so that we can check if we should wait for the + // the categories map to be un-initialized. + await SpecialPowers.popPrefEnv(); + if ( + !Services.prefs.getBoolPref( + "browser.search.serpEventTelemetryCategorization.enabled" + ) + ) { + await waitForDomainToCategoriesUninit(); + } else { + // The scheduler uses the mock idle service. + SearchSERPCategorizationEventScheduler.uninit(); + SearchSERPCategorizationEventScheduler.init(); + } CATEGORIZATION_SETTINGS.WAKE_TIMEOUT_MS = oldWakeTimeout; SearchSERPTelemetry.overrideSearchTelemetryForTests(); resetTelemetry(); @@ -138,6 +149,7 @@ add_task(async function test_categorize_serp_and_sleep() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }, @@ -195,6 +207,7 @@ add_task(async function test_categorize_serp_and_sleep_not_long_enough() { partner_code: "ff", provider: "example", tagged: "true", + is_shopping_page: "false", num_ads_clicked: "0", num_ads_visible: "2", }, diff --git a/browser/components/search/test/browser/telemetry/head.js b/browser/components/search/test/browser/telemetry/head.js index b798099bdd..ecc6e38fa9 100644 --- a/browser/components/search/test/browser/telemetry/head.js +++ b/browser/components/search/test/browser/telemetry/head.js @@ -4,11 +4,14 @@ ChromeUtils.defineESModuleGetters(this, { ADLINK_CHECK_TIMEOUT_MS: "resource:///actors/SearchSERPTelemetryChild.sys.mjs", + CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs", CustomizableUITestUtils: "resource://testing-common/CustomizableUITestUtils.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", SEARCH_TELEMETRY_SHARED: "resource:///modules/SearchSERPTelemetry.sys.mjs", + SearchSERPDomainToCategoriesMap: + "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", @@ -193,11 +196,10 @@ async function assertSearchSourcesTelemetry( } function resetTelemetry() { - // TODO Bug 1868476: Replace when we're using Glean telemetry. - fakeTelemetryStorage = []; searchCounts.clear(); Services.telemetry.clearScalars(); Services.fog.testResetFOG(); + SERPCategorizationRecorder.testReset(); } /** @@ -377,23 +379,6 @@ function assertSERPTelemetry(expectedEvents) { ); } -// TODO Bug 1868476: Replace when we're using Glean telemetry. -let categorizationSandbox; -let fakeTelemetryStorage = []; -add_setup(function () { - categorizationSandbox = sinon.createSandbox(); - categorizationSandbox - .stub(SERPCategorizationRecorder, "recordCategorizationTelemetry") - .callsFake(input => { - fakeTelemetryStorage.push(input); - }); - - registerCleanupFunction(() => { - categorizationSandbox.restore(); - fakeTelemetryStorage = []; - }); -}); - async function openSerpInNewTab(url, expectedAds = true) { let promise; if (expectedAds) { @@ -435,12 +420,11 @@ async function synthesizePageAction({ } function assertCategorizationValues(expectedResults) { - // TODO Bug 1868476: Replace with calls to Glean telemetry. - let actualResults = [...fakeTelemetryStorage]; + let actualResults = Glean.serp.categorization.testGetValue() ?? []; Assert.equal( - expectedResults.length, actualResults.length, + expectedResults.length, "Should have the correct number of categorization impressions." ); @@ -458,7 +442,7 @@ function assertCategorizationValues(expectedResults) { } } for (let actual of actualResults) { - for (let key in actual) { + for (let key in actual.extra) { keys.add(key); } } @@ -467,14 +451,21 @@ function assertCategorizationValues(expectedResults) { for (let index = 0; index < expectedResults.length; ++index) { info(`Checking categorization at index: ${index}`); let expected = expectedResults[index]; - let actual = actualResults[index]; + let actual = actualResults[index].extra; + + Assert.ok( + Number(actual?.organic_num_domains) <= + CATEGORIZATION_SETTINGS.MAX_DOMAINS_TO_CATEGORIZE, + "Number of organic domains categorized should not exceed threshold." + ); + + Assert.ok( + Number(actual?.sponsored_num_domains) <= + CATEGORIZATION_SETTINGS.MAX_DOMAINS_TO_CATEGORIZE, + "Number of sponsored domains categorized should not exceed threshold." + ); + for (let key of keys) { - // TODO Bug 1868476: This conversion to strings is to mimic Glean - // converting all values into strings. Once we receive real values from - // Glean, it can be removed. - if (actual[key] != null && typeof actual[key] !== "string") { - actual[key] = actual[key].toString(); - } Assert.equal( actual[key], expected[key], @@ -508,6 +499,14 @@ function waitForDomainToCategoriesUpdate() { return TestUtils.topicObserved("domain-to-categories-map-update-complete"); } +function waitForDomainToCategoriesInit() { + return TestUtils.topicObserved("domain-to-categories-map-init"); +} + +function waitForDomainToCategoriesUninit() { + return TestUtils.topicObserved("domain-to-categories-map-uninit"); +} + registerCleanupFunction(async () => { await PlacesUtils.history.clear(); }); diff --git a/browser/components/search/test/browser/telemetry/searchTelemetryDomainCategorizationReportingWithoutAds.html b/browser/components/search/test/browser/telemetry/searchTelemetryDomainCategorizationReportingWithoutAds.html new file mode 100644 index 0000000000..13d023e45d --- /dev/null +++ b/browser/components/search/test/browser/telemetry/searchTelemetryDomainCategorizationReportingWithoutAds.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Document</title> +</head> +<body> + <div> + <a id="shopping" href="https://www.example.org/shopping">Shopping</a> + </div> + <div id="results"> + <div class="organic"> + <a href="https://www.foobar.org">Link</a> + </div> + </div> +</body> +</html> diff --git a/browser/components/search/test/browser/telemetry/searchTelemetryDomainExtraction.html b/browser/components/search/test/browser/telemetry/searchTelemetryDomainExtraction.html index 28c31af959..fe52bb8b48 100644 --- a/browser/components/search/test/browser/telemetry/searchTelemetryDomainExtraction.html +++ b/browser/components/search/test/browser/telemetry/searchTelemetryDomainExtraction.html @@ -256,6 +256,37 @@ </div> </div> </div> + + <div id="test26"> + <div id="b_results"> + <div class="b_algo"> + <div class="b_attribution"> + <cite>https://organic.com/cats</cite> + </div> + </div> + </div> + </div> + + <div id="test27"> + <div id="b_results"> + <div class="b_algo"> + <div class="b_attribution"> + <cite>https://organic.com/testing?q=cats</cite> + </div> + </div> + </div> + </div> + + <div id="test28"> + <div id="b_results"> + <div class="b_algo"> + <div class="b_attribution"> + <span>HTTPS</span> + <cite>en.wikipedia.org/wiki/Cat</cite> + </div> + </div> + </div> + </div> </div> </body> </html> diff --git a/browser/components/search/test/marionette/manifest.toml b/browser/components/search/test/marionette/manifest.toml index 152442bc5b..9cc88e9f84 100644 --- a/browser/components/search/test/marionette/manifest.toml +++ b/browser/components/search/test/marionette/manifest.toml @@ -1,4 +1,6 @@ [DEFAULT] run-if = ["buildapp == 'browser'"] +["include:telemetry/manifest.toml"] + ["test_engines_on_restart.py"] diff --git a/browser/components/search/test/marionette/telemetry/manifest.toml b/browser/components/search/test/marionette/telemetry/manifest.toml new file mode 100644 index 0000000000..1fe35945c9 --- /dev/null +++ b/browser/components/search/test/marionette/telemetry/manifest.toml @@ -0,0 +1,4 @@ +[DEFAULT] +run-if = ["buildapp == 'browser'"] + +["test_ping_submitted.py"] diff --git a/browser/components/search/test/marionette/telemetry/test_ping_submitted.py b/browser/components/search/test/marionette/telemetry/test_ping_submitted.py new file mode 100644 index 0000000000..cefe2d72d1 --- /dev/null +++ b/browser/components/search/test/marionette/telemetry/test_ping_submitted.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# 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/. + +from marionette_driver import Wait +from marionette_harness.marionette_test import MarionetteTestCase + + +class TestPingSubmitted(MarionetteTestCase): + def setUp(self): + super(TestPingSubmitted, self).setUp() + + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + self.marionette.enforce_gecko_prefs( + { + "datareporting.healthreport.uploadEnabled": True, + "telemetry.fog.test.localhost_port": 3000, + "browser.search.log": True, + } + ) + # The categorization ping is submitted on startup. If anything delays + # its initialization, turning the preference on and immediately + # attaching a categorization event could result in the ping being + # submitted after the test event is reported but before the browser + # restarts. + script = """ + let [outerResolve] = arguments; + (async () => { + if (!Services.prefs.getBoolPref("browser.search.serpEventTelemetryCategorization.enabled")) { + let inited = new Promise(innerResolve => { + Services.obs.addObserver(function callback() { + Services.obs.removeObserver(callback, "categorization-recorder-init"); + innerResolve(); + }, "categorization-recorder-init"); + }); + Services.prefs.setBoolPref("browser.search.serpEventTelemetryCategorization.enabled", true); + await inited; + } + })().then(outerResolve); + """ + self.marionette.execute_async_script(script) + + def test_ping_submit_on_start(self): + # Record an event for the ping to eventually submit. + self.marionette.execute_script( + """ + Glean.serp.categorization.record({ + organic_category: "3", + organic_num_domains: "1", + organic_num_inconclusive: "0", + organic_num_unknown: "0", + sponsored_category: "4", + sponsored_num_domains: "2", + sponsored_num_inconclusive: "0", + sponsored_num_unknown: "0", + mappings_version: "1", + app_version: "124", + channel: "nightly", + region: "US", + partner_code: "ff", + provider: "example", + tagged: "true", + num_ads_clicked: "0", + num_ads_visible: "2", + }); + """ + ) + + Wait(self.marionette, timeout=60).until( + lambda _: self.marionette.execute_script( + """ + return (Glean.serp.categorization.testGetValue()?.length ?? 0) == 1; + """ + ), + message="Should have recorded a SERP categorization event before restart.", + ) + + self.marionette.restart(clean=False, in_app=True) + + Wait(self.marionette, timeout=60).until( + lambda _: self.marionette.execute_script( + """ + return (Glean.serp.categorization.testGetValue()?.length ?? 0) == 0; + """ + ), + message="SERP categorization should have been sent some time after restart.", + ) diff --git a/browser/components/search/test/unit/corruptDB.sqlite b/browser/components/search/test/unit/corruptDB.sqlite Binary files differnew file mode 100644 index 0000000000..b234246cac --- /dev/null +++ b/browser/components/search/test/unit/corruptDB.sqlite diff --git a/browser/components/search/test/unit/test_domain_to_categories_store.js b/browser/components/search/test/unit/test_domain_to_categories_store.js new file mode 100644 index 0000000000..e3af0c8de5 --- /dev/null +++ b/browser/components/search/test/unit/test_domain_to_categories_store.js @@ -0,0 +1,361 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Ensure that the domain to categories store public methods work as expected + * and it handles all error cases as expected. + */ + +ChromeUtils.defineESModuleGetters(this, { + CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs", + DomainToCategoriesStore: "resource:///modules/SearchSERPTelemetry.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +let store = new DomainToCategoriesStore(); +let defaultStorePath; +let fileContents = [convertToBuffer({ foo: [0, 1] })]; + +async function createCorruptedStore() { + info("Create a corrupted store."); + let storePath = PathUtils.join( + PathUtils.profileDir, + CATEGORIZATION_SETTINGS.STORE_FILE + ); + let src = PathUtils.join(do_get_cwd().path, "corruptDB.sqlite"); + await IOUtils.copy(src, storePath); + Assert.ok(await IOUtils.exists(storePath), "Store exists."); + return storePath; +} + +function convertToBuffer(obj) { + return new TextEncoder().encode(JSON.stringify(obj)).buffer; +} + +/** + * Deletes data from the store and removes any files that were generated due + * to them. + */ +async function cleanup() { + info("Clean up store."); + + // In these tests, we sometimes use read-only files to test permission error + // handling. On Windows, we have to change it to writable to allow for their + // deletion so that subsequent tests aren't affected. + if ( + (await IOUtils.exists(defaultStorePath)) && + Services.appinfo.OS == "WINNT" + ) { + await IOUtils.setPermissions(defaultStorePath, 0o600); + } + + await store.testDelete(); + Assert.equal(store.empty, true, "Store should be empty."); + Assert.equal(await IOUtils.exists(defaultStorePath), false, "Store exists."); + Assert.equal( + await store.getVersion(), + 0, + "Version number should be 0 when store is empty." + ); + + await store.uninit(); +} + +async function createReadOnlyStore() { + info("Create a store that can't be read."); + let storePath = PathUtils.join( + PathUtils.profileDir, + CATEGORIZATION_SETTINGS.STORE_FILE + ); + + let conn = await Sqlite.openConnection({ path: storePath }); + await conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); + await conn.close(); + + await changeStoreToReadOnly(); +} + +async function changeStoreToReadOnly() { + info("Change store to read only."); + let storePath = PathUtils.join( + PathUtils.profileDir, + CATEGORIZATION_SETTINGS.STORE_FILE + ); + let stat = await IOUtils.stat(storePath); + await IOUtils.setPermissions(storePath, 0o444); + stat = await IOUtils.stat(storePath); + Assert.equal(stat.permissions, 0o444, "Permissions should be read only."); + Assert.ok(await IOUtils.exists(storePath), "Store exists."); +} + +add_setup(async function () { + // We need a profile directory to create the store and open a connection. + do_get_profile(); + defaultStorePath = PathUtils.join( + PathUtils.profileDir, + CATEGORIZATION_SETTINGS.STORE_FILE + ); + registerCleanupFunction(async () => { + await cleanup(); + }); +}); + +// Ensure the test only function deletes the store. +add_task(async function delete_store() { + let storePath = await createCorruptedStore(); + await store.testDelete(); + Assert.ok(!(await IOUtils.exists(storePath)), "Store doesn't exist."); +}); + +/** + * These tests check common no fail scenarios. + */ + +add_task(async function init_insert_uninit() { + await store.init(); + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [], "foo should not have a result."); + Assert.equal( + await store.getVersion(), + 0, + "Version number should not be set." + ); + Assert.equal(store.empty, true, "Store should be empty."); + + info("Try inserting after init."); + await store.insertFileContents(fileContents, 1); + + result = await store.getCategories("foo"); + Assert.deepEqual(result, [0, 1], "foo should have a matching result."); + Assert.equal(await store.getVersion(), 1, "Version number should be set."); + Assert.equal(store.empty, false, "Store should not be empty."); + + info("Un-init store."); + await store.uninit(); + + result = await store.getCategories("foo"); + Assert.deepEqual(result, [], "foo should be removed from store."); + Assert.equal(store.empty, true, "Store should be empty."); + Assert.equal(await store.getVersion(), 0, "Version should be reset."); + + await cleanup(); +}); + +add_task(async function insert_and_re_init() { + await store.init(); + await store.insertFileContents(fileContents, 20240202); + + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [0, 1], "foo should have a matching result."); + Assert.equal( + await store.getVersion(), + 20240202, + "Version number should be set." + ); + Assert.equal(store.empty, false, "Is store empty."); + + info("Simulate a restart."); + await store.uninit(); + await store.init(); + + result = await store.getCategories("foo"); + Assert.deepEqual( + result, + [0, 1], + "After restart, foo should still be in the store." + ); + Assert.equal( + await store.getVersion(), + 20240202, + "Version number should still be in the store." + ); + Assert.equal(store.empty, false, "Is store empty."); + + await cleanup(); +}); + +// Simulate consecutive updates. +add_task(async function insert_multiple_times() { + await store.init(); + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [], "foo should not have a result."); + Assert.equal( + await store.getVersion(), + 0, + "Version number should not be set." + ); + Assert.equal(store.empty, true, "Is store empty."); + + for (let i = 0; i < 3; ++i) { + info("Try inserting after init."); + await store.insertFileContents(fileContents, 1); + + result = await store.getCategories("foo"); + Assert.deepEqual(result, [0, 1], "foo should have a matching result."); + Assert.equal(store.empty, false, "Is store empty."); + Assert.equal(await store.getVersion(), 1, "Version number is set."); + + await store.dropData(); + result = await store.getCategories("foo"); + Assert.deepEqual( + result, + [], + "After dropping data, foo should no longer have a matching result." + ); + Assert.equal(await store.getVersion(), 0, "Version should be reset."); + Assert.equal(store.empty, true, "Is store empty."); + } + + await cleanup(); +}); + +/** + * The following tests check failures on store initialization. + */ + +add_task(async function init_with_corrupted_store() { + await createCorruptedStore(); + + info("Initialize the store."); + await store.init(); + + info("Try inserting after the corrupted store was replaced."); + await store.insertFileContents(fileContents, 1); + + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [0, 1], "foo should have a matching result."); + Assert.equal(await store.getVersion(), 1, "Version number is set."); + Assert.equal(store.empty, false, "Is store empty."); + + await cleanup(); +}); + +add_task(async function init_with_unfixable_store() { + let sandbox = sinon.createSandbox(); + sandbox.stub(Sqlite, "openConnection").throws(); + + info("Initialize the store."); + await store.init(); + + info("Try inserting content even if the connection is impossible to fix."); + await store.dropData(); + await store.insertFileContents(fileContents, 20240202); + + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [], "foo should not have a result."); + Assert.equal(await store.getVersion(), 0, "Version should be reset."); + Assert.equal(store.empty, true, "Store should be empty."); + + sandbox.restore(); + await cleanup(); +}); + +add_task(async function init_read_only_store() { + await createReadOnlyStore(); + await store.init(); + + info("Insert contents into the store."); + await store.insertFileContents(fileContents, 20240202); + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [], "foo should not have a result."); + Assert.equal( + await store.getVersion(), + 0, + "Version number should not be set." + ); + Assert.equal(store.empty, true, "Store should be empty."); + + await cleanup(); +}); + +add_task(async function init_close_to_shutdown() { + let sandbox = sinon.createSandbox(); + sandbox.stub(Sqlite.shutdown, "addBlocker").throws(new Error()); + await store.init(); + + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [], "foo should not have a result."); + Assert.equal( + await store.getVersion(), + 0, + "Version number should not be set." + ); + Assert.equal(store.empty, true, "Store should be empty."); + + sandbox.restore(); + await cleanup(); +}); + +/** + * The following tests check error handling when inserting data into the store. + */ + +add_task(async function insert_broken_file() { + await store.init(); + + Assert.equal( + await store.getVersion(), + 0, + "Version number should not be set." + ); + + info("Try inserting one valid file and an invalid file."); + let contents = [...fileContents, new ArrayBuffer(0).buffer]; + await store.insertFileContents(contents, 20240202); + + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [], "foo should not have a result."); + Assert.equal(await store.getVersion(), 0, "Version should remain unset."); + Assert.equal(store.empty, true, "Store should remain empty."); + + await cleanup(); +}); + +add_task(async function insert_into_read_only_store() { + await createReadOnlyStore(); + await store.init(); + + await store.dropData(); + await store.insertFileContents(fileContents, 20240202); + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [], "foo should not have a result."); + Assert.equal(await store.getVersion(), 0, "Version should remain unset."); + Assert.equal(store.empty, true, "Store should remain empty."); + + await cleanup(); +}); + +// If the store becomes read only with content already inside of it, +// the next time we try opening it, we'll encounter an error trying to write to +// it. Since we are no longer able to manipulate it, the results should always +// be empty. +add_task(async function restart_with_read_only_store() { + await store.init(); + await store.insertFileContents(fileContents, 20240202); + + info("Check store has content."); + let result = await store.getCategories("foo"); + Assert.deepEqual(result, [0, 1], "foo should have a matching result."); + Assert.equal( + await store.getVersion(), + 20240202, + "Version number should be set." + ); + Assert.equal(store.empty, false, "Store should not be empty."); + + await changeStoreToReadOnly(); + await store.uninit(); + await store.init(); + + result = await store.getCategories("foo"); + Assert.deepEqual( + result, + [], + "foo should no longer have a matching value from the store." + ); + Assert.equal(await store.getVersion(), 0, "Version number should be unset."); + Assert.equal(store.empty, true, "Store should be empty."); + + await cleanup(); +}); diff --git a/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js b/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js index 40d38efbba..2351347d77 100644 --- a/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js +++ b/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js @@ -9,6 +9,7 @@ ChromeUtils.defineESModuleGetters(this, { RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + SearchSERPCategorization: "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchSERPDomainToCategoriesMap: "resource:///modules/SearchSERPTelemetry.sys.mjs", TELEMETRY_CATEGORIZATION_KEY: @@ -158,7 +159,7 @@ add_task(async function test_initial_import() { // Clean up. await db.clear(); - SearchSERPDomainToCategoriesMap.uninit(); + await SearchSERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_update_records() { @@ -219,7 +220,7 @@ add_task(async function test_update_records() { // Clean up. await db.clear(); - SearchSERPDomainToCategoriesMap.uninit(); + await SearchSERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_delayed_initial_import() { @@ -273,7 +274,7 @@ add_task(async function test_delayed_initial_import() { // Clean up. await db.clear(); - SearchSERPDomainToCategoriesMap.uninit(); + await SearchSERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_remove_record() { @@ -332,7 +333,7 @@ add_task(async function test_remove_record() { // Clean up. await db.clear(); - SearchSERPDomainToCategoriesMap.uninit(); + await SearchSERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_different_versions_coexisting() { @@ -380,7 +381,7 @@ add_task(async function test_different_versions_coexisting() { // Clean up. await db.clear(); - SearchSERPDomainToCategoriesMap.uninit(); + await SearchSERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_download_error() { @@ -449,5 +450,67 @@ add_task(async function test_download_error() { // Clean up. await db.clear(); - SearchSERPDomainToCategoriesMap.uninit(); + await SearchSERPDomainToCategoriesMap.uninit(true); +}); + +add_task(async function test_mock_restart() { + info("Create record containing domain_category_mappings_2a.json attachment."); + let record2a = await mockRecordWithCachedAttachment(RECORDS.record2a); + await db.create(record2a); + + info("Create record containing domain_category_mappings_2b.json attachment."); + let record2b = await mockRecordWithCachedAttachment(RECORDS.record2b); + await db.create(record2b); + + info("Add data to Remote Settings DB."); + await db.importChanges({}, Date.now()); + + info("Initialize search categorization mappings."); + let promise = waitForDomainToCategoriesUpdate(); + await SearchSERPCategorization.init(); + await promise; + + Assert.deepEqual( + await SearchSERPDomainToCategoriesMap.get("example.com"), + [ + { + category: 1, + score: 80, + }, + ], + "Should have a record." + ); + + Assert.equal( + SearchSERPDomainToCategoriesMap.version, + 2, + "Version should be the latest." + ); + + info("Mock a restart by un-initializing the map."); + await SearchSERPCategorization.uninit(); + promise = waitForDomainToCategoriesUpdate(); + await SearchSERPCategorization.init(); + await promise; + + Assert.deepEqual( + await SearchSERPDomainToCategoriesMap.get("example.com"), + [ + { + category: 1, + score: 80, + }, + ], + "Should have a record." + ); + + Assert.equal( + SearchSERPDomainToCategoriesMap.version, + 2, + "Version should be the latest." + ); + + // Clean up. + await db.clear(); + await SearchSERPDomainToCategoriesMap.uninit(true); }); diff --git a/browser/components/search/test/unit/test_search_telemetry_config_validation.js b/browser/components/search/test/unit/test_search_telemetry_config_validation.js index 8897b1e7c7..d14f7a3918 100644 --- a/browser/components/search/test/unit/test_search_telemetry_config_validation.js +++ b/browser/components/search/test/unit/test_search_telemetry_config_validation.js @@ -57,7 +57,7 @@ function disallowAdditionalProperties(section) { add_task(async function test_search_telemetry_validates_to_schema() { let schema = await IOUtils.readJSON( - PathUtils.join(do_get_cwd().path, "search-telemetry-schema.json") + PathUtils.join(do_get_cwd().path, "search-telemetry-v2-schema.json") ); disallowAdditionalProperties(schema); diff --git a/browser/components/search/test/unit/test_ui_schemas_valid.js b/browser/components/search/test/unit/test_ui_schemas_valid.js new file mode 100644 index 0000000000..3396f38238 --- /dev/null +++ b/browser/components/search/test/unit/test_ui_schemas_valid.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let schemas = [ + ["search-telemetry-v2-schema.json", "search-telemetry-v2-ui-schema.json"], +]; + +async function checkUISchemaValid(configSchema, uiSchema) { + for (let key of Object.keys(configSchema.properties)) { + Assert.ok( + uiSchema["ui:order"].includes(key), + `Should have ${key} listed at the top-level of the ui schema` + ); + } +} + +add_task(async function test_ui_schemas_valid() { + for (let [schema, uiSchema] of schemas) { + info(`Validating ${uiSchema} has every top-level from ${schema}`); + let schemaData = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, schema) + ); + let uiSchemaData = await IOUtils.readJSON( + PathUtils.join(do_get_cwd().path, uiSchema) + ); + + await checkUISchemaValid(schemaData, uiSchemaData); + } +}); diff --git a/browser/components/search/test/unit/xpcshell.toml b/browser/components/search/test/unit/xpcshell.toml index 423d218d19..24e1d78eb5 100644 --- a/browser/components/search/test/unit/xpcshell.toml +++ b/browser/components/search/test/unit/xpcshell.toml @@ -6,6 +6,9 @@ prefs = ["browser.search.log=true"] skip-if = ["os == 'android'"] # bug 1730213 firefox-appdir = "browser" +["test_domain_to_categories_store.js"] +support-files = ["corruptDB.sqlite"] + ["test_search_telemetry_categorization_logic.js"] ["test_search_telemetry_categorization_sync.js"] @@ -14,7 +17,13 @@ prefs = ["browser.search.serpEventTelemetryCategorization.enabled=true"] ["test_search_telemetry_compare_urls.js"] ["test_search_telemetry_config_validation.js"] -support-files = ["../../schema/search-telemetry-schema.json"] +support-files = ["../../schema/search-telemetry-v2-schema.json"] + +["test_ui_schemas_valid.js"] +support-files = [ + "../../schema/search-telemetry-v2-schema.json", + "../../schema/search-telemetry-v2-ui-schema.json", +] ["test_urlTelemetry.js"] diff --git a/browser/components/sessionstore/ContentRestore.sys.mjs b/browser/components/sessionstore/ContentRestore.sys.mjs deleted file mode 100644 index e55772cab3..0000000000 --- a/browser/components/sessionstore/ContentRestore.sys.mjs +++ /dev/null @@ -1,435 +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/. */ - -const lazy = {}; - -ChromeUtils.defineESModuleGetters(lazy, { - SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs", - Utils: "resource://gre/modules/sessionstore/Utils.sys.mjs", -}); - -/** - * This module implements the content side of session restoration. The chrome - * side is handled by SessionStore.sys.mjs. The functions in this module are called - * by content-sessionStore.js based on messages received from SessionStore.sys.mjs - * (or, in one case, based on a "load" event). Each tab has its own - * ContentRestore instance, constructed by content-sessionStore.js. - * - * In a typical restore, content-sessionStore.js will call the following based - * on messages and events it receives: - * - * restoreHistory(tabData, loadArguments, callbacks) - * Restores the tab's history and session cookies. - * restoreTabContent(loadArguments, finishCallback) - * Starts loading the data for the current page to restore. - * restoreDocument() - * Restore form and scroll data. - * - * When the page has been loaded from the network, we call finishCallback. It - * should send a message to SessionStore.sys.mjs, which may cause other tabs to be - * restored. - * - * When the page has finished loading, a "load" event will trigger in - * content-sessionStore.js, which will call restoreDocument. At that point, - * form data is restored and the restore is complete. - * - * At any time, SessionStore.sys.mjs can cancel the ongoing restore by sending a - * reset message, which causes resetRestore to be called. At that point it's - * legal to begin another restore. - */ -export function ContentRestore(chromeGlobal) { - let internal = new ContentRestoreInternal(chromeGlobal); - let external = {}; - - let EXPORTED_METHODS = [ - "restoreHistory", - "restoreTabContent", - "restoreDocument", - "resetRestore", - ]; - - for (let method of EXPORTED_METHODS) { - external[method] = internal[method].bind(internal); - } - - return Object.freeze(external); -} - -function ContentRestoreInternal(chromeGlobal) { - this.chromeGlobal = chromeGlobal; - - // The following fields are only valid during certain phases of the restore - // process. - - // The tabData for the restore. Set in restoreHistory and removed in - // restoreTabContent. - this._tabData = null; - - // Contains {entry, scrollPositions, formdata}, where entry is a - // single entry from the tabData.entries array. Set in - // restoreTabContent and removed in restoreDocument. - this._restoringDocument = null; - - // This listener is used to detect reloads on restoring tabs. Set in - // restoreHistory and removed in restoreTabContent. - this._historyListener = null; - - // This listener detects when a pending tab starts loading (when not - // initiated by sessionstore) and when a restoring tab has finished loading - // data from the network. Set in restoreHistory() and restoreTabContent(), - // removed in resetRestore(). - this._progressListener = null; -} - -/** - * The API for the ContentRestore module. Methods listed in EXPORTED_METHODS are - * public. - */ -ContentRestoreInternal.prototype = { - get docShell() { - return this.chromeGlobal.docShell; - }, - - /** - * Starts the process of restoring a tab. The tabData to be restored is passed - * in here and used throughout the restoration. The epoch (which must be - * non-zero) is passed through to all the callbacks. If a load in the tab - * is started while it is pending, the appropriate callbacks are called. - */ - restoreHistory(tabData, loadArguments, callbacks) { - this._tabData = tabData; - - // In case about:blank isn't done yet. - let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); - webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL); - - // Make sure currentURI is set so that switch-to-tab works before the tab is - // restored. We'll reset this to about:blank when we try to restore the tab - // to ensure that docshell doeesn't get confused. Don't bother doing this if - // we're restoring immediately due to a process switch. It just causes the - // URL bar to be temporarily blank. - let activeIndex = tabData.index - 1; - let activePageData = tabData.entries[activeIndex] || {}; - let uri = activePageData.url || null; - if (uri && !loadArguments) { - webNavigation.setCurrentURIForSessionStore(Services.io.newURI(uri)); - } - - lazy.SessionHistory.restore(this.docShell, tabData); - - // Add a listener to watch for reloads. - let listener = new HistoryListener(this.docShell, () => { - // On reload, restore tab contents. - this.restoreTabContent(null, false, callbacks.onLoadFinished); - }); - - webNavigation.sessionHistory.legacySHistory.addSHistoryListener(listener); - this._historyListener = listener; - - // Make sure to reset the capabilities and attributes in case this tab gets - // reused. - SessionStoreUtils.restoreDocShellCapabilities( - this.docShell, - tabData.disallow - ); - - // Add a progress listener to correctly handle browser.loadURI() - // calls from foreign code. - this._progressListener = new ProgressListener(this.docShell, { - onStartRequest: () => { - // Some code called browser.loadURI() on a pending tab. It's safe to - // assume we don't care about restoring scroll or form data. - this._tabData = null; - - // Listen for the tab to finish loading. - this.restoreTabContentStarted(callbacks.onLoadFinished); - - // Notify the parent. - callbacks.onLoadStarted(); - }, - }); - }, - - /** - * Start loading the current page. When the data has finished loading from the - * network, finishCallback is called. Returns true if the load was successful. - */ - restoreTabContent(loadArguments, isRemotenessUpdate, finishCallback) { - let tabData = this._tabData; - this._tabData = null; - - let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation); - - // Listen for the tab to finish loading. - this.restoreTabContentStarted(finishCallback); - - // Reset the current URI to about:blank. We changed it above for - // switch-to-tab, but now it must go back to the correct value before the - // load happens. Don't bother doing this if we're restoring immediately - // due to a process switch. - if (!isRemotenessUpdate) { - webNavigation.setCurrentURIForSessionStore( - Services.io.newURI("about:blank") - ); - } - - try { - if (loadArguments) { - // If the load was started in another process, and the in-flight channel - // was redirected into this process, resume that load within our process. - // - // NOTE: In this case `isRemotenessUpdate` must be true. - webNavigation.resumeRedirectedLoad( - loadArguments.redirectLoadSwitchId, - loadArguments.redirectHistoryIndex - ); - } else if (tabData.userTypedValue && tabData.userTypedClear) { - // If the user typed a URL into the URL bar and hit enter right before - // we crashed, we want to start loading that page again. A non-zero - // userTypedClear value means that the load had started. - // Load userTypedValue and fix up the URL if it's partial/broken. - let loadURIOptions = { - triggeringPrincipal: - Services.scriptSecurityManager.getSystemPrincipal(), - loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, - }; - webNavigation.fixupAndLoadURIString( - tabData.userTypedValue, - loadURIOptions - ); - } else if (tabData.entries.length) { - // Stash away the data we need for restoreDocument. - this._restoringDocument = { - formdata: tabData.formdata || {}, - scrollPositions: tabData.scroll || {}, - }; - - // In order to work around certain issues in session history, we need to - // force session history to update its internal index and call reload - // instead of gotoIndex. See bug 597315. - let history = webNavigation.sessionHistory.legacySHistory; - history.reloadCurrentEntry(); - } else { - // If there's nothing to restore, we should still blank the page. - let loadURIOptions = { - triggeringPrincipal: - Services.scriptSecurityManager.getSystemPrincipal(), - loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, - // Specify an override to force the load to finish in the current - // process, as tests rely on this behaviour for non-fission session - // restore. - remoteTypeOverride: Services.appinfo.remoteType, - }; - webNavigation.loadURI( - Services.io.newURI("about:blank"), - loadURIOptions - ); - } - - return true; - } catch (ex) { - if (ex instanceof Ci.nsIException) { - // Ignore page load errors, but return false to signal that the load never - // happened. - return false; - } - } - return null; - }, - - /** - * To be called after restoreHistory(). Removes all listeners needed for - * pending tabs and makes sure to notify when the tab finished loading. - */ - restoreTabContentStarted(finishCallback) { - // The reload listener is no longer needed. - this._historyListener.uninstall(); - this._historyListener = null; - - // Remove the old progress listener. - this._progressListener.uninstall(); - - // We're about to start a load. This listener will be called when the load - // has finished getting everything from the network. - this._progressListener = new ProgressListener(this.docShell, { - onStopRequest: () => { - // Call resetRestore() to reset the state back to normal. The data - // needed for restoreDocument() (which hasn't happened yet) will - // remain in _restoringDocument. - this.resetRestore(); - - finishCallback(); - }, - }); - }, - - /** - * Finish restoring the tab by filling in form data and setting the scroll - * position. The restore is complete when this function exits. It should be - * called when the "load" event fires for the restoring tab. Returns true - * if we're restoring a document. - */ - restoreDocument() { - if (!this._restoringDocument) { - return; - } - - let { formdata, scrollPositions } = this._restoringDocument; - this._restoringDocument = null; - - let window = this.docShell.domWindow; - - // Restore form data. - lazy.Utils.restoreFrameTreeData(window, formdata, (frame, data) => { - // restore() will return false, and thus abort restoration for the - // current |frame| and its descendants, if |data.url| is given but - // doesn't match the loaded document's URL. - return SessionStoreUtils.restoreFormData(frame.document, data); - }); - - // Restore scroll data. - lazy.Utils.restoreFrameTreeData(window, scrollPositions, (frame, data) => { - if (data.scroll) { - SessionStoreUtils.restoreScrollPosition(frame, data); - } - }); - }, - - /** - * Cancel an ongoing restore. This function can be called any time between - * restoreHistory and restoreDocument. - * - * This function is called externally (if a restore is canceled) and - * internally (when the loads for a restore have finished). In the latter - * case, it's called before restoreDocument, so it cannot clear - * _restoringDocument. - */ - resetRestore() { - this._tabData = null; - - if (this._historyListener) { - this._historyListener.uninstall(); - } - this._historyListener = null; - - if (this._progressListener) { - this._progressListener.uninstall(); - } - this._progressListener = null; - }, -}; - -/* - * This listener detects when a page being restored is reloaded. It triggers a - * callback and cancels the reload. The callback will send a message to - * SessionStore.sys.mjs so that it can restore the content immediately. - */ -function HistoryListener(docShell, callback) { - let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation); - webNavigation.sessionHistory.legacySHistory.addSHistoryListener(this); - - this.webNavigation = webNavigation; - this.callback = callback; -} -HistoryListener.prototype = { - QueryInterface: ChromeUtils.generateQI([ - "nsISHistoryListener", - "nsISupportsWeakReference", - ]), - - uninstall() { - let shistory = this.webNavigation.sessionHistory.legacySHistory; - if (shistory) { - shistory.removeSHistoryListener(this); - } - }, - - OnHistoryGotoIndex() {}, - OnHistoryPurge() {}, - OnHistoryReplaceEntry() {}, - - // This will be called for a pending tab when loadURI(uri) is called where - // the given |uri| only differs in the fragment. - OnHistoryNewEntry(newURI) { - let currentURI = this.webNavigation.currentURI; - - // Ignore new SHistory entries with the same URI as those do not indicate - // a navigation inside a document by changing the #hash part of the URL. - // We usually hit this when purging session history for browsers. - if (currentURI && currentURI.spec == newURI.spec) { - return; - } - - // Reset the tab's URL to what it's actually showing. Without this loadURI() - // would use the current document and change the displayed URL only. - this.webNavigation.setCurrentURIForSessionStore( - Services.io.newURI("about:blank") - ); - - // Kick off a new load so that we navigate away from about:blank to the - // new URL that was passed to loadURI(). The new load will cause a - // STATE_START notification to be sent and the ProgressListener will then - // notify the parent and do the rest. - let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; - let loadURIOptions = { - triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), - loadFlags, - }; - this.webNavigation.loadURI(newURI, loadURIOptions); - }, - - OnHistoryReload() { - this.callback(); - - // Cancel the load. - return false; - }, -}; - -/** - * This class informs SessionStore.sys.mjs whenever the network requests for a - * restoring page have completely finished. We only restore three tabs - * simultaneously, so this is the signal for SessionStore.sys.mjs to kick off - * another restore (if there are more to do). - * - * The progress listener is also used to be notified when a load not initiated - * by sessionstore starts. Pending tabs will then need to be marked as no - * longer pending. - */ -function ProgressListener(docShell, callbacks) { - let webProgress = docShell - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); - webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW); - - this.webProgress = webProgress; - this.callbacks = callbacks; -} - -ProgressListener.prototype = { - QueryInterface: ChromeUtils.generateQI([ - "nsIWebProgressListener", - "nsISupportsWeakReference", - ]), - - uninstall() { - this.webProgress.removeProgressListener(this); - }, - - onStateChange(webProgress, request, stateFlags, status) { - let { STATE_IS_WINDOW, STATE_STOP, STATE_START } = - Ci.nsIWebProgressListener; - if (!webProgress.isTopLevel || !(stateFlags & STATE_IS_WINDOW)) { - return; - } - - if (stateFlags & STATE_START && this.callbacks.onStartRequest) { - this.callbacks.onStartRequest(); - } - - if (stateFlags & STATE_STOP && this.callbacks.onStopRequest) { - this.callbacks.onStopRequest(); - } - }, -}; diff --git a/browser/components/sessionstore/ContentSessionStore.sys.mjs b/browser/components/sessionstore/ContentSessionStore.sys.mjs deleted file mode 100644 index 44f59cd39d..0000000000 --- a/browser/components/sessionstore/ContentSessionStore.sys.mjs +++ /dev/null @@ -1,685 +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 { - clearTimeout, - setTimeoutWithTarget, -} from "resource://gre/modules/Timer.sys.mjs"; - -const lazy = {}; - -ChromeUtils.defineESModuleGetters(lazy, { - ContentRestore: "resource:///modules/sessionstore/ContentRestore.sys.mjs", - SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs", -}); - -// This pref controls whether or not we send updates to the parent on a timeout -// or not, and should only be used for tests or debugging. -const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates"; - -const PREF_INTERVAL = "browser.sessionstore.interval"; - -const kNoIndex = Number.MAX_SAFE_INTEGER; -const kLastIndex = Number.MAX_SAFE_INTEGER - 1; - -class Handler { - constructor(store) { - this.store = store; - } - - get contentRestore() { - return this.store.contentRestore; - } - - get contentRestoreInitialized() { - return this.store.contentRestoreInitialized; - } - - get mm() { - return this.store.mm; - } - - get messageQueue() { - return this.store.messageQueue; - } -} - -/** - * Listens for and handles content events that we need for the - * session store service to be notified of state changes in content. - */ -class EventListener extends Handler { - constructor(store) { - super(store); - - SessionStoreUtils.addDynamicFrameFilteredListener( - this.mm, - "load", - this, - true - ); - } - - handleEvent(event) { - let { content } = this.mm; - - // Ignore load events from subframes. - if (event.target != content.document) { - return; - } - - if (content.document.documentURI.startsWith("about:reader")) { - if ( - event.type == "load" && - !content.document.body.classList.contains("loaded") - ) { - // Don't restore the scroll position of an about:reader page at this - // point; listen for the custom event dispatched from AboutReader.sys.mjs. - content.addEventListener("AboutReaderContentReady", this); - return; - } - - content.removeEventListener("AboutReaderContentReady", this); - } - - if (this.contentRestoreInitialized) { - // Restore the form data and scroll position. - this.contentRestore.restoreDocument(); - } - } -} - -/** - * Listens for changes to the session history. Whenever the user navigates - * we will collect URLs and everything belonging to session history. - * - * Causes a SessionStore:update message to be sent that contains the current - * session history. - * - * Example: - * {entries: [{url: "about:mozilla", ...}, ...], index: 1} - */ -class SessionHistoryListener extends Handler { - constructor(store) { - super(store); - - this._fromIdx = kNoIndex; - - // By adding the SHistoryListener immediately, we will unfortunately be - // notified of every history entry as the tab is restored. We don't bother - // waiting to add the listener later because these notifications are cheap. - // We will likely only collect once since we are batching collection on - // a delay. - this.mm.docShell - .QueryInterface(Ci.nsIWebNavigation) - .sessionHistory.legacySHistory.addSHistoryListener(this); // OK in non-geckoview - - let webProgress = this.mm.docShell - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); - - webProgress.addProgressListener( - this, - Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT - ); - - // Collect data if we start with a non-empty shistory. - if (!lazy.SessionHistory.isEmpty(this.mm.docShell)) { - this.collect(); - // When a tab is detached from the window, for the new window there is a - // new SessionHistoryListener created. Normally it is empty at this point - // but in a test env. the initial about:blank might have a children in which - // case we fire off a history message here with about:blank in it. If we - // don't do it ASAP then there is going to be a browser swap and the parent - // will be all confused by that message. - this.store.messageQueue.send(); - } - - // Listen for page title changes. - this.mm.addEventListener("DOMTitleChanged", this); - } - - get mm() { - return this.store.mm; - } - - uninit() { - let sessionHistory = this.mm.docShell.QueryInterface( - Ci.nsIWebNavigation - ).sessionHistory; - if (sessionHistory) { - sessionHistory.legacySHistory.removeSHistoryListener(this); // OK in non-geckoview - } - } - - collect() { - // We want to send down a historychange even for full collects in case our - // session history is a partial session history, in which case we don't have - // enough information for a full update. collectFrom(-1) tells the collect - // function to collect all data avaliable in this process. - if (this.mm.docShell) { - this.collectFrom(-1); - } - } - - // History can grow relatively big with the nested elements, so if we don't have to, we - // don't want to send the entire history all the time. For a simple optimization - // we keep track of the smallest index from after any change has occured and we just send - // the elements from that index. If something more complicated happens we just clear it - // and send the entire history. We always send the additional info like the current selected - // index (so for going back and forth between history entries we set the index to kLastIndex - // if nothing else changed send an empty array and the additonal info like the selected index) - collectFrom(idx) { - if (this._fromIdx <= idx) { - // If we already know that we need to update history fromn index N we can ignore any changes - // tha happened with an element with index larger than N. - // Note: initially we use kNoIndex which is MAX_SAFE_INTEGER which means we don't ignore anything - // here, and in case of navigation in the history back and forth we use kLastIndex which ignores - // only the subsequent navigations, but not any new elements added. - return; - } - - this._fromIdx = idx; - this.store.messageQueue.push("historychange", () => { - if (this._fromIdx === kNoIndex) { - return null; - } - - let history = lazy.SessionHistory.collect( - this.mm.docShell, - this._fromIdx - ); - this._fromIdx = kNoIndex; - return history; - }); - } - - handleEvent(event) { - this.collect(); - } - - OnHistoryNewEntry(newURI, oldIndex) { - // Collect the current entry as well, to make sure to collect any changes - // that were made to the entry while the document was active. - this.collectFrom(oldIndex == -1 ? oldIndex : oldIndex - 1); - } - - OnHistoryGotoIndex() { - // We ought to collect the previously current entry as well, see bug 1350567. - this.collectFrom(kLastIndex); - } - - OnHistoryPurge() { - this.collect(); - } - - OnHistoryReload() { - this.collect(); - return true; - } - - OnHistoryReplaceEntry() { - this.collect(); - } - - /** - * @see nsIWebProgressListener.onStateChange - */ - onStateChange(webProgress, request, stateFlags, status) { - // Ignore state changes for subframes because we're only interested in the - // top-document starting or stopping its load. - if (!webProgress.isTopLevel || webProgress.DOMWindow != this.mm.content) { - return; - } - - // onStateChange will be fired when loading the initial about:blank URI for - // a browser, which we don't actually care about. This is particularly for - // the case of unrestored background tabs, where the content has not yet - // been restored: we don't want to accidentally send any updates to the - // parent when the about:blank placeholder page has loaded. - if (!this.mm.docShell.hasLoadedNonBlankURI) { - return; - } - - if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { - this.collect(); - } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { - this.collect(); - } - } -} -SessionHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([ - "nsIWebProgressListener", - "nsISHistoryListener", - "nsISupportsWeakReference", -]); - -/** - * A message queue that takes collected data and will take care of sending it - * to the chrome process. It allows flushing using synchronous messages and - * takes care of any race conditions that might occur because of that. Changes - * will be batched if they're pushed in quick succession to avoid a message - * flood. - */ -class MessageQueue extends Handler { - constructor(store) { - super(store); - - /** - * A map (string -> lazy fn) holding lazy closures of all queued data - * collection routines. These functions will return data collected from the - * docShell. - */ - this._data = new Map(); - - /** - * The delay (in ms) used to delay sending changes after data has been - * invalidated. - */ - this.BATCH_DELAY_MS = 1000; - - /** - * The minimum idle period (in ms) we need for sending data to chrome process. - */ - this.NEEDED_IDLE_PERIOD_MS = 5; - - /** - * Timeout for waiting an idle period to send data. We will set this from - * the pref "browser.sessionstore.interval". - */ - this._timeoutWaitIdlePeriodMs = null; - - /** - * The current timeout ID, null if there is no queue data. We use timeouts - * to damp a flood of data changes and send lots of changes as one batch. - */ - this._timeout = null; - - /** - * Whether or not sending batched messages on a timer is disabled. This should - * only be used for debugging or testing. If you need to access this value, - * you should probably use the timeoutDisabled getter. - */ - this._timeoutDisabled = false; - - /** - * True if there is already a send pending idle dispatch, set to prevent - * scheduling more than one. If false there may or may not be one scheduled. - */ - this._idleScheduled = false; - - this.timeoutDisabled = Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF); - this._timeoutWaitIdlePeriodMs = Services.prefs.getIntPref(PREF_INTERVAL); - - Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this); - Services.prefs.addObserver(PREF_INTERVAL, this); - } - - /** - * True if batched messages are not being fired on a timer. This should only - * ever be true when debugging or during tests. - */ - get timeoutDisabled() { - return this._timeoutDisabled; - } - - /** - * Disables sending batched messages on a timer. Also cancels any pending - * timers. - */ - set timeoutDisabled(val) { - this._timeoutDisabled = val; - - if (val && this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - } - } - - uninit() { - Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this); - Services.prefs.removeObserver(PREF_INTERVAL, this); - this.cleanupTimers(); - } - - /** - * Cleanup pending idle callback and timer. - */ - cleanupTimers() { - this._idleScheduled = false; - if (this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - } - } - - observe(subject, topic, data) { - if (topic == "nsPref:changed") { - switch (data) { - case TIMEOUT_DISABLED_PREF: - this.timeoutDisabled = Services.prefs.getBoolPref( - TIMEOUT_DISABLED_PREF - ); - break; - case PREF_INTERVAL: - this._timeoutWaitIdlePeriodMs = - Services.prefs.getIntPref(PREF_INTERVAL); - break; - default: - console.error("received unknown message '" + data + "'"); - break; - } - } - } - - /** - * Pushes a given |value| onto the queue. The given |key| represents the type - * of data that is stored and can override data that has been queued before - * but has not been sent to the parent process, yet. - * - * @param key (string) - * A unique identifier specific to the type of data this is passed. - * @param fn (function) - * A function that returns the value that will be sent to the parent - * process. - */ - push(key, fn) { - this._data.set(key, fn); - - if (!this._timeout && !this._timeoutDisabled) { - // Wait a little before sending the message to batch multiple changes. - this._timeout = setTimeoutWithTarget( - () => this.sendWhenIdle(), - this.BATCH_DELAY_MS, - this.mm.tabEventTarget - ); - } - } - - /** - * Sends queued data when the remaining idle time is enough or waiting too - * long; otherwise, request an idle time again. If the |deadline| is not - * given, this function is going to schedule the first request. - * - * @param deadline (object) - * An IdleDeadline object passed by idleDispatch(). - */ - sendWhenIdle(deadline) { - if (!this.mm.content) { - // The frameloader is being torn down. Nothing more to do. - return; - } - - if (deadline) { - if ( - deadline.didTimeout || - deadline.timeRemaining() > this.NEEDED_IDLE_PERIOD_MS - ) { - this.send(); - return; - } - } else if (this._idleScheduled) { - // Bail out if there's a pending run. - return; - } - ChromeUtils.idleDispatch(deadline_ => this.sendWhenIdle(deadline_), { - timeout: this._timeoutWaitIdlePeriodMs, - }); - this._idleScheduled = true; - } - - /** - * Sends queued data to the chrome process. - * - * @param options (object) - * {flushID: 123} to specify that this is a flush - * {isFinal: true} to signal this is the final message sent on unload - */ - send(options = {}) { - // Looks like we have been called off a timeout after the tab has been - // closed. The docShell is gone now and we can just return here as there - // is nothing to do. - if (!this.mm.docShell) { - return; - } - - this.cleanupTimers(); - - let flushID = (options && options.flushID) || 0; - let histID = "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_MS"; - - let data = {}; - for (let [key, func] of this._data) { - if (key != "isPrivate") { - TelemetryStopwatch.startKeyed(histID, key); - } - - let value = func(); - - if (key != "isPrivate") { - TelemetryStopwatch.finishKeyed(histID, key); - } - - if (value || (key != "storagechange" && key != "historychange")) { - data[key] = value; - } - } - - this._data.clear(); - - try { - // Send all data to the parent process. - this.mm.sendAsyncMessage("SessionStore:update", { - data, - flushID, - isFinal: options.isFinal || false, - epoch: this.store.epoch, - }); - } catch (ex) { - if (ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) { - Services.telemetry - .getHistogramById("FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM") - .add(1); - this.mm.sendAsyncMessage("SessionStore:error"); - } - } - } -} - -/** - * Listens for and handles messages sent by the session store service. - */ -const MESSAGES = [ - "SessionStore:restoreHistory", - "SessionStore:restoreTabContent", - "SessionStore:resetRestore", - "SessionStore:flush", - "SessionStore:prepareForProcessChange", -]; - -export class ContentSessionStore { - constructor(mm) { - if (Services.appinfo.sessionHistoryInParent) { - throw new Error("This frame script should not be loaded for SHIP"); - } - - this.mm = mm; - this.messageQueue = new MessageQueue(this); - - this.epoch = 0; - - this.contentRestoreInitialized = false; - - this.handlers = [ - this.messageQueue, - new EventListener(this), - new SessionHistoryListener(this), - ]; - - ChromeUtils.defineLazyGetter(this, "contentRestore", () => { - this.contentRestoreInitialized = true; - return new lazy.ContentRestore(mm); - }); - - MESSAGES.forEach(m => mm.addMessageListener(m, this)); - - mm.addEventListener("unload", this); - } - - receiveMessage({ name, data }) { - // The docShell might be gone. Don't process messages, - // that will just lead to errors anyway. - if (!this.mm.docShell) { - return; - } - - // A fresh tab always starts with epoch=0. The parent has the ability to - // override that to signal a new era in this tab's life. This enables it - // to ignore async messages that were already sent but not yet received - // and would otherwise confuse the internal tab state. - if (data && data.epoch && data.epoch != this.epoch) { - this.epoch = data.epoch; - } - - switch (name) { - case "SessionStore:restoreHistory": - this.restoreHistory(data); - break; - case "SessionStore:restoreTabContent": - this.restoreTabContent(data); - break; - case "SessionStore:resetRestore": - this.contentRestore.resetRestore(); - break; - case "SessionStore:flush": - this.flush(data); - break; - case "SessionStore:prepareForProcessChange": - // During normal in-process navigations, the DocShell would take - // care of automatically persisting layout history state to record - // scroll positions on the nsSHEntry. Unfortunately, process switching - // is not a normal navigation, so for now we do this ourselves. This - // is a workaround until session history state finally lives in the - // parent process. - this.mm.docShell.persistLayoutHistoryState(); - break; - default: - console.error("received unknown message '" + name + "'"); - break; - } - } - - // non-SHIP only - restoreHistory(data) { - let { epoch, tabData, loadArguments, isRemotenessUpdate } = data; - - this.contentRestore.restoreHistory(tabData, loadArguments, { - // Note: The callbacks passed here will only be used when a load starts - // that was not initiated by sessionstore itself. This can happen when - // some code calls browser.loadURI() or browser.reload() on a pending - // browser/tab. - - onLoadStarted: () => { - // Notify the parent that the tab is no longer pending. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", { - epoch, - }); - }, - - onLoadFinished: () => { - // Tell SessionStore.sys.mjs that it may want to restore some more tabs, - // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", { - epoch, - }); - }, - }); - - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { - // For non-remote tabs, when restoreHistory finishes, we send a synchronous - // message to SessionStore.sys.mjs so that it can run SSTabRestoring. Users of - // SSTabRestoring seem to get confused if chrome and content are out of - // sync about the state of the restore (particularly regarding - // docShell.currentURI). Using a synchronous message is the easiest way - // to temporarily synchronize them. - // - // For remote tabs, because all nsIWebProgress notifications are sent - // asynchronously using messages, we get the same-order guarantees of the - // message manager, and can use an async message. - this.mm.sendSyncMessage("SessionStore:restoreHistoryComplete", { - epoch, - isRemotenessUpdate, - }); - } else { - this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", { - epoch, - isRemotenessUpdate, - }); - } - } - - restoreTabContent({ loadArguments, isRemotenessUpdate, reason }) { - let epoch = this.epoch; - - // We need to pass the value of didStartLoad back to SessionStore.sys.mjs. - let didStartLoad = this.contentRestore.restoreTabContent( - loadArguments, - isRemotenessUpdate, - () => { - // Tell SessionStore.sys.mjs that it may want to restore some more tabs, - // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", { - epoch, - isRemotenessUpdate, - }); - } - ); - - this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", { - epoch, - isRemotenessUpdate, - reason, - }); - - if (!didStartLoad) { - // Pretend that the load succeeded so that event handlers fire correctly. - this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", { - epoch, - isRemotenessUpdate, - }); - } - } - - flush({ id }) { - // Flush the message queue, send the latest updates. - this.messageQueue.send({ flushID: id }); - } - - handleEvent(event) { - if (event.type == "unload") { - this.onUnload(); - } - } - - onUnload() { - // Upon frameLoader destruction, send a final update message to - // the parent and flush all data currently held in the child. - this.messageQueue.send({ isFinal: true }); - - for (let handler of this.handlers) { - if (handler.uninit) { - handler.uninit(); - } - } - - if (this.contentRestoreInitialized) { - // Remove progress listeners. - this.contentRestore.resetRestore(); - } - - // We don't need to take care of any StateChangeNotifier observers as they - // will die with the content script. The same goes for the privacy transition - // observer that will die with the docShell when the tab is closed. - } -} diff --git a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs index 4d53b166c0..d0627180f0 100644 --- a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs +++ b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs @@ -182,7 +182,7 @@ export var RecentlyClosedTabsAndWindowsMenuUtils = { * @param aEvent * The command event when the user clicks the restore all menu item */ - onRestoreAllWindowsCommand(aEvent) { + onRestoreAllWindowsCommand() { const count = lazy.SessionStore.getClosedWindowCount(); for (let index = 0; index < count; index++) { lazy.SessionStore.undoCloseWindow(index); @@ -265,7 +265,7 @@ function createEntry( element.removeAttribute("oncommand"); element.addEventListener( "command", - event => { + () => { lazy.SessionStore.undoClosedTabFromClosedWindow( { sourceClosedId }, aClosedTab.closedId diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs index 1e5a3bf718..077529d739 100644 --- a/browser/components/sessionstore/SessionFile.sys.mjs +++ b/browser/components/sessionstore/SessionFile.sys.mjs @@ -194,6 +194,7 @@ var SessionFileInternal = { }, async _readInternal(useOldExtension) { + Services.telemetry.setEventRecordingEnabled("session_restore", true); let result; let noFilesFound = true; this._usingOldExtension = useOldExtension; @@ -251,6 +252,18 @@ var SessionFileInternal = { path, ". Wrong format/version: " + JSON.stringify(parsed.version) + "." ); + Services.telemetry.recordEvent( + "session_restore", + "backup_can_be_loaded", + "session_file", + null, + { + can_load: "false", + path_key: key, + loadfail_reason: + "Wrong format/version: " + JSON.stringify(parsed.version) + ".", + } + ); continue; } result = { @@ -259,6 +272,17 @@ var SessionFileInternal = { parsed, useOldExtension, }; + Services.telemetry.recordEvent( + "session_restore", + "backup_can_be_loaded", + "session_file", + null, + { + can_load: "true", + path_key: key, + loadfail_reason: "N/A", + } + ); Services.telemetry .getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE") .add(false); @@ -269,6 +293,17 @@ var SessionFileInternal = { } catch (ex) { if (DOMException.isInstance(ex) && ex.name == "NotFoundError") { exists = false; + Services.telemetry.recordEvent( + "session_restore", + "backup_can_be_loaded", + "session_file", + null, + { + can_load: "false", + path_key: key, + loadfail_reason: "File doesn't exist.", + } + ); } else if ( DOMException.isInstance(ex) && ex.name == "NotAllowedError" @@ -277,6 +312,17 @@ var SessionFileInternal = { // or similar failures. We'll just count it as "corrupted". console.error("Could not read session file ", ex); corrupted = true; + Services.telemetry.recordEvent( + "session_restore", + "backup_can_be_loaded", + "session_file", + null, + { + can_load: "false", + path_key: key, + loadfail_reason: ` ${ex.name}: Could not read session file`, + } + ); } else if (ex instanceof SyntaxError) { console.error( "Corrupt session file (invalid JSON found) ", @@ -285,6 +331,17 @@ var SessionFileInternal = { ); // File is corrupted, try next file corrupted = true; + Services.telemetry.recordEvent( + "session_restore", + "backup_can_be_loaded", + "session_file", + null, + { + can_load: "false", + path_key: key, + loadfail_reason: ` ${ex.name}: Corrupt session file (invalid JSON found)`, + } + ); } } finally { if (exists) { @@ -292,6 +349,17 @@ var SessionFileInternal = { Services.telemetry .getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE") .add(corrupted); + Services.telemetry.recordEvent( + "session_restore", + "backup_can_be_loaded", + "session_file", + null, + { + can_load: (!corrupted).toString(), + path_key: key, + loadfail_reason: "N/A", + } + ); } } } diff --git a/browser/components/sessionstore/SessionSaver.sys.mjs b/browser/components/sessionstore/SessionSaver.sys.mjs index 2f08bb2243..1237e3f970 100644 --- a/browser/components/sessionstore/SessionSaver.sys.mjs +++ b/browser/components/sessionstore/SessionSaver.sys.mjs @@ -210,7 +210,7 @@ var SessionSaverInternal = { /** * Observe idle/ active notifications. */ - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "idle": this._isIdle = true; diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs index ff3ba55176..0d017ac035 100644 --- a/browser/components/sessionstore/SessionStartup.sys.mjs +++ b/browser/components/sessionstore/SessionStartup.sys.mjs @@ -158,6 +158,11 @@ export var SessionStartup = { */ _onSessionFileRead({ source, parsed, noFilesFound }) { this._initialized = true; + const crashReasons = { + FINAL_STATE_WRITING_INCOMPLETE: "final-state-write-incomplete", + SESSION_STATE_FLAG_MISSING: + "session-state-missing-or-running-at-last-write", + }; // Let observers modify the state before it is used let supportsStateString = this._createSupportsString(source); @@ -210,12 +215,17 @@ export var SessionStartup = { delete this._initialState.lastSessionState; } + let previousSessionCrashedReason = "N/A"; lazy.CrashMonitor.previousCheckpoints.then(checkpoints => { if (checkpoints) { // If the previous session finished writing the final state, we'll // assume there was no crash. this._previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"]; + if (!checkpoints["sessionstore-final-state-write-complete"]) { + previousSessionCrashedReason = + crashReasons.FINAL_STATE_WRITING_INCOMPLETE; + } } else if (noFilesFound) { // If the Crash Monitor could not load a checkpoints file it will // provide null. This could occur on the first run after updating to @@ -241,6 +251,13 @@ export var SessionStartup = { this._previousSessionCrashed = !stateFlagPresent || this._initialState.session.state == STATE_RUNNING_STR; + if ( + !stateFlagPresent || + this._initialState.session.state == STATE_RUNNING_STR + ) { + previousSessionCrashedReason = + crashReasons.SESSION_STATE_FLAG_MISSING; + } } // Report shutdown success via telemetry. Shortcoming here are @@ -249,6 +266,16 @@ export var SessionStartup = { Services.telemetry .getHistogramById("SHUTDOWN_OK") .add(!this._previousSessionCrashed); + Services.telemetry.recordEvent( + "session_restore", + "shutdown_success", + "session_startup", + null, + { + shutdown_ok: this._previousSessionCrashed.toString(), + shutdown_reason: previousSessionCrashedReason, + } + ); Services.obs.addObserver(this, "sessionstore-windows-restored", true); @@ -268,7 +295,7 @@ export var SessionStartup = { /** * Handle notifications */ - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "sessionstore-windows-restored": Services.obs.removeObserver(this, "sessionstore-windows-restored"); diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs index f269251f54..16137b8388 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -109,59 +109,6 @@ const WINDOW_OPEN_FEATURES_MAP = { statusbar: "status", }; -// Messages that will be received via the Frame Message Manager. -const MESSAGES = [ - // The content script sends us data that has been invalidated and needs to - // be saved to disk. - "SessionStore:update", - - // The restoreHistory code has run. This is a good time to run SSTabRestoring. - "SessionStore:restoreHistoryComplete", - - // The load for the restoring tab has begun. We update the URL bar at this - // time; if we did it before, the load would overwrite it. - "SessionStore:restoreTabContentStarted", - - // All network loads for a restoring tab are done, so we should - // consider restoring another tab in the queue. The document has - // been restored, and forms have been filled. We trigger - // SSTabRestored at this time. - "SessionStore:restoreTabContentComplete", - - // The content script encountered an error. - "SessionStore:error", -]; - -// The list of messages we accept from <xul:browser>s that have no tab -// assigned, or whose windows have gone away. Those are for example the -// ones that preload about:newtab pages, or from browsers where the window -// has just been closed. -const NOTAB_MESSAGES = new Set([ - // For a description see above. - "SessionStore:update", - - // For a description see above. - "SessionStore:error", -]); - -// The list of messages we accept without an "epoch" parameter. -// See getCurrentEpoch() and friends to find out what an "epoch" is. -const NOEPOCH_MESSAGES = new Set([ - // For a description see above. - "SessionStore:error", -]); - -// The list of messages we want to receive even during the short period after a -// frame has been removed from the DOM and before its frame script has finished -// unloading. -const CLOSED_MESSAGES = new Set([ - // For a description see above. - "SessionStore:update", - - // For a description see above. - "SessionStore:error", -]); - // These are tab events that we listen to. const TAB_EVENTS = [ "TabOpen", @@ -645,10 +592,6 @@ export var SessionStore = { SessionStoreInternal.deleteCustomGlobalValue(aKey); }, - persistTabAttribute: function ss_persistTabAttribute(aName) { - SessionStoreInternal.persistTabAttribute(aName); - }, - restoreLastSession: function ss_restoreLastSession() { SessionStoreInternal.restoreLastSession(); }, @@ -813,18 +756,6 @@ export var SessionStore = { }, /** - * Prepares to change the remoteness of the given browser, by ensuring that - * the local instance of session history is up-to-date. - */ - async prepareToChangeRemoteness(aTab) { - await SessionStoreInternal.prepareToChangeRemoteness(aTab); - }, - - finishTabRemotenessChange(aTab, aSwitchId) { - SessionStoreInternal.finishTabRemotenessChange(aTab, aSwitchId); - }, - - /** * Clear session store data for a given private browsing window. * @param {ChromeWindow} win - Open private browsing window to clear data for. */ @@ -1354,8 +1285,6 @@ var SessionStoreInternal = { "privacy.resistFingerprinting" ); Services.prefs.addObserver("privacy.resistFingerprinting", this); - - this._shistoryInParent = Services.appinfo.sessionHistoryInParent; }, /** @@ -1434,33 +1363,26 @@ var SessionStoreInternal = { } break; case "browsing-context-did-set-embedder": - if (Services.appinfo.sessionHistoryInParent) { - if ( - aSubject && - aSubject === aSubject.top && - aSubject.isContent && - aSubject.embedderElement && - aSubject.embedderElement.permanentKey - ) { - let permanentKey = aSubject.embedderElement.permanentKey; - this._browserSHistoryListener.get(permanentKey)?.unregister(); - this.getOrCreateSHistoryListener(permanentKey, aSubject, true); - } + if ( + aSubject && + aSubject === aSubject.top && + aSubject.isContent && + aSubject.embedderElement && + aSubject.embedderElement.permanentKey + ) { + let permanentKey = aSubject.embedderElement.permanentKey; + this._browserSHistoryListener.get(permanentKey)?.unregister(); + this.getOrCreateSHistoryListener(permanentKey, aSubject, true); } break; case "browsing-context-discarded": - if (Services.appinfo.sessionHistoryInParent) { - let permanentKey = aSubject?.embedderElement?.permanentKey; - if (permanentKey) { - this._browserSHistoryListener.get(permanentKey)?.unregister(); - } + let permanentKey = aSubject?.embedderElement?.permanentKey; + if (permanentKey) { + this._browserSHistoryListener.get(permanentKey)?.unregister(); } break; case "browser-shutdown-tabstate-updated": - if (Services.appinfo.sessionHistoryInParent) { - // Non-SHIP code calls this when the frame script is unloaded. - this.onFinalTabStateUpdateComplete(aSubject); - } + this.onFinalTabStateUpdateComplete(aSubject); this._notifyOfClosedObjectsChange(); break; } @@ -1573,10 +1495,6 @@ var SessionStoreInternal = { } } - if (!Services.appinfo.sessionHistoryInParent) { - throw new Error("This function should only be used with SHIP"); - } - if (!permanentKey || browsingContext !== browsingContext.top) { return null; } @@ -1691,29 +1609,27 @@ var SessionStoreInternal = { return; } - if (Services.appinfo.sessionHistoryInParent) { - let listener = this.getOrCreateSHistoryListener( - permanentKey, - browsingContext - ); + let listener = this.getOrCreateSHistoryListener( + permanentKey, + browsingContext + ); - if (listener) { - let historychange = - // If it is not the scheduled update (tab closed, window closed etc), - // try to store the loading non-web-controlled page opened in _blank - // first. - (forStorage && - lazy.SessionHistory.collectNonWebControlledBlankLoadingSession( - browsingContext - )) || - listener.collect(permanentKey, browsingContext, { - collectFull: !!update.sHistoryNeeded, - writeToCache: false, - }); + if (listener) { + let historychange = + // If it is not the scheduled update (tab closed, window closed etc), + // try to store the loading non-web-controlled page opened in _blank + // first. + (forStorage && + lazy.SessionHistory.collectNonWebControlledBlankLoadingSession( + browsingContext + )) || + listener.collect(permanentKey, browsingContext, { + collectFull: !!update.sHistoryNeeded, + writeToCache: false, + }); - if (historychange) { - update.data.historychange = historychange; - } + if (historychange) { + update.data.historychange = historychange; } } @@ -1724,98 +1640,6 @@ var SessionStoreInternal = { this.onTabStateUpdate(permanentKey, win, update); }, - /** - * This method handles incoming messages sent by the session store content - * script via the Frame Message Manager or Parent Process Message Manager, - * and thus enables communication with OOP tabs. - */ - receiveMessage(aMessage) { - if (Services.appinfo.sessionHistoryInParent) { - throw new Error( - `received unexpected message '${aMessage.name}' with ` + - `sessionHistoryInParent enabled` - ); - } - - // If we got here, that means we're dealing with a frame message - // manager message, so the target will be a <xul:browser>. - var browser = aMessage.target; - let win = browser.ownerGlobal; - let tab = win ? win.gBrowser.getTabForBrowser(browser) : null; - - // Ensure we receive only specific messages from <xul:browser>s that - // have no tab or window assigned, e.g. the ones that preload - // about:newtab pages, or windows that have closed. - if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) { - throw new Error( - `received unexpected message '${aMessage.name}' ` + - `from a browser that has no tab or window` - ); - } - - let data = aMessage.data || {}; - let hasEpoch = data.hasOwnProperty("epoch"); - - // Most messages sent by frame scripts require to pass an epoch. - if (!hasEpoch && !NOEPOCH_MESSAGES.has(aMessage.name)) { - throw new Error(`received message '${aMessage.name}' without an epoch`); - } - - // Ignore messages from previous epochs. - if (hasEpoch && !this.isCurrentEpoch(browser.permanentKey, data.epoch)) { - return; - } - - switch (aMessage.name) { - case "SessionStore:update": - // |browser.frameLoader| might be empty if the browser was already - // destroyed and its tab removed. In that case we still have the last - // frameLoader we know about to compare. - let frameLoader = - browser.frameLoader || - this._lastKnownFrameLoader.get(browser.permanentKey); - - // If the message isn't targeting the latest frameLoader discard it. - if (frameLoader != aMessage.targetFrameLoader) { - return; - } - - this.onTabStateUpdate(browser.permanentKey, browser.ownerGlobal, data); - - // SHIP code will call this when it receives "browser-shutdown-tabstate-updated" - if (data.isFinal) { - if (!Services.appinfo.sessionHistoryInParent) { - this.onFinalTabStateUpdateComplete(browser); - } - } else if (data.flushID) { - // This is an update kicked off by an async flush request. Notify the - // TabStateFlusher so that it can finish the request and notify its - // consumer that's waiting for the flush to be done. - lazy.TabStateFlusher.resolve(browser, data.flushID); - } - - break; - case "SessionStore:restoreHistoryComplete": - this._restoreHistoryComplete(browser, data); - break; - case "SessionStore:restoreTabContentStarted": - this._restoreTabContentStarted(browser, data); - break; - case "SessionStore:restoreTabContentComplete": - this._restoreTabContentComplete(browser, data); - break; - case "SessionStore:error": - lazy.TabStateFlusher.resolveAll( - browser, - false, - "Received error from the content process" - ); - break; - default: - throw new Error(`received unknown message '${aMessage.name}'`); - } - }, - /* ........ Window Event Handlers .............. */ /** @@ -1917,21 +1741,6 @@ var SessionStoreInternal = { // internal data about the window. aWindow.__SSi = this._generateWindowID(); - if (!Services.appinfo.sessionHistoryInParent) { - let mm = aWindow.getGroupMessageManager("browsers"); - MESSAGES.forEach(msg => { - let listenWhenClosed = CLOSED_MESSAGES.has(msg); - mm.addMessageListener(msg, this, listenWhenClosed); - }); - - // Load the frame script after registering listeners. - mm.loadFrameScript( - "chrome://browser/content/content-sessionStore.js", - true, - true - ); - } - // and create its data object this._windows[aWindow.__SSi] = { tabs: [], @@ -2347,7 +2156,7 @@ var SessionStoreInternal = { // Save non-private windows if they have at // least one saveable tab or are the last window. if (!winData.isPrivate) { - this.maybeSaveClosedWindow(winData, isLastWindow, true); + this.maybeSaveClosedWindow(winData, isLastWindow); if (!isLastWindow && winData.closedId > -1) { this._addClosedAction( @@ -2402,11 +2211,6 @@ var SessionStoreInternal = { // Cache the window state until it is completely gone. DyingWindowCache.set(aWindow, winData); - if (!Services.appinfo.sessionHistoryInParent) { - let mm = aWindow.getGroupMessageManager("browsers"); - MESSAGES.forEach(msg => mm.removeMessageListener(msg, this)); - } - this._saveableClosedWindowData.delete(winData); delete aWindow.__SSi; }, @@ -2428,7 +2232,7 @@ var SessionStoreInternal = { * to call this method again asynchronously (for example, after * a window flush). */ - maybeSaveClosedWindow(winData, isLastWindow, recordTelemetry = false) { + maybeSaveClosedWindow(winData, isLastWindow) { // Make sure SessionStore is still running, and make sure that we // haven't chosen to forget this window. if ( @@ -2489,13 +2293,9 @@ var SessionStoreInternal = { this._removeClosedWindow(winIndex); return; } - // we only do this after the TabStateFlusher promise resolves in ssi_onClose - if (recordTelemetry) { - let closedTabsHistogram = Services.telemetry.getHistogramById( - "FX_SESSION_RESTORE_CLOSED_TABS_NOT_SAVED" - ); - closedTabsHistogram.add(winData._closedTabs.length); - } + this._log.warn( + `Discarding window with 0 saveable tabs and ${winData._closedTabs.length} closed tabs` + ); } } }, @@ -3644,7 +3444,7 @@ var SessionStoreInternal = { } // Create a new tab. - let userContextId = aTab.getAttribute("usercontextid"); + let userContextId = aTab.getAttribute("usercontextid") || ""; let tabOptions = { userContextId, @@ -4273,12 +4073,6 @@ var SessionStoreInternal = { this.saveStateDelayed(); }, - persistTabAttribute: function ssi_persistTabAttribute(aName) { - if (lazy.TabAttributes.persist(aName)) { - this.saveStateDelayed(); - } - }, - /** * Undoes the closing of a tab or window which corresponds * to the closedId passed in. @@ -5480,13 +5274,6 @@ var SessionStoreInternal = { tab.updateLastAccessed(tabData.lastAccessed); } - if ("attributes" in tabData) { - // Ensure that we persist tab attributes restored from previous sessions. - Object.keys(tabData.attributes).forEach(a => - lazy.TabAttributes.persist(a) - ); - } - if (!tabData.entries) { tabData.entries = []; } @@ -5656,7 +5443,6 @@ var SessionStoreInternal = { let browser = aTab.linkedBrowser; let window = aTab.ownerGlobal; - let tabbrowser = window.gBrowser; let tabData = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab)); let activeIndex = tabData.index - 1; let activePageData = tabData.entries[activeIndex] || null; @@ -5664,36 +5450,9 @@ var SessionStoreInternal = { this.markTabAsRestoring(aTab); - let isRemotenessUpdate = aOptions.isRemotenessUpdate; - let explicitlyUpdateRemoteness = !Services.appinfo.sessionHistoryInParent; - // If we aren't already updating the browser's remoteness, check if it's - // necessary. - if (explicitlyUpdateRemoteness && !isRemotenessUpdate) { - isRemotenessUpdate = tabbrowser.updateBrowserRemotenessByURL( - browser, - uri - ); - - if (isRemotenessUpdate) { - // We updated the remoteness, so we need to send the history down again. - // - // Start a new epoch to discard all frame script messages relating to a - // previous epoch. All async messages that are still on their way to chrome - // will be ignored and don't override any tab data set when restoring. - let epoch = this.startNextEpoch(browser.permanentKey); - - this._sendRestoreHistory(browser, { - tabData, - epoch, - loadArguments, - isRemotenessUpdate, - }); - } - } - this._sendRestoreTabContent(browser, { loadArguments, - isRemotenessUpdate, + isRemotenessUpdate: aOptions.isRemotenessUpdate, reason: aOptions.restoreContentReason || RESTORE_TAB_CONTENT_REASON.SET_STATE, }); @@ -6828,10 +6587,8 @@ var SessionStoreInternal = { // The browser is no longer in any sort of restoring state. TAB_STATE_FOR_BROWSER.delete(browser); - if (Services.appinfo.sessionHistoryInParent) { - this._restoreListeners.get(browser.permanentKey)?.unregister(); - browser.browsingContext.clearRestoreState(); - } + this._restoreListeners.get(browser.permanentKey)?.unregister(); + browser.browsingContext.clearRestoreState(); aTab.removeAttribute("pending"); @@ -6855,9 +6612,6 @@ var SessionStoreInternal = { return; } - if (!Services.appinfo.sessionHistoryInParent) { - browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {}); - } this._resetLocalTabRestoringState(tab); }, @@ -7048,7 +6802,7 @@ var SessionStoreInternal = { } catch {} // May have already gotten rid of the browser's webProgress. }, - onStateChange(webProgress, request, stateFlags, status) { + onStateChange(webProgress, request, stateFlags) { if ( webProgress.isTopLevel && stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW && @@ -7109,7 +6863,7 @@ var SessionStoreInternal = { OnHistoryPurge() {}, OnHistoryReplaceEntry() {}, - onStateChange(webProgress, request, stateFlags, status) { + onStateChange(webProgress, request, stateFlags) { if ( webProgress.isTopLevel && stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW && @@ -7143,10 +6897,6 @@ var SessionStoreInternal = { * history restores. */ _restoreHistory(browser, data) { - if (!Services.appinfo.sessionHistoryInParent) { - throw new Error("This function should only be used with SHIP"); - } - this._tabStateToRestore.set(browser.permanentKey, data); // In case about:blank isn't done yet. @@ -7190,7 +6940,7 @@ var SessionStoreInternal = { this._tabStateRestorePromises.delete(browser.permanentKey); - this._restoreHistoryComplete(browser, data); + this._restoreHistoryComplete(browser); }; promise.then(onResolve).catch(() => {}); @@ -7238,10 +6988,6 @@ var SessionStoreInternal = { * history restores. */ _restoreTabContent(browser, options = {}) { - if (!Services.appinfo.sessionHistoryInParent) { - throw new Error("This function should only be used with SHIP"); - } - this._restoreListeners.get(browser.permanentKey)?.unregister(); this._restoreTabContentStarted(browser, options); @@ -7266,17 +7012,10 @@ var SessionStoreInternal = { }, _sendRestoreTabContent(browser, options) { - if (Services.appinfo.sessionHistoryInParent) { - this._restoreTabContent(browser, options); - } else { - browser.messageManager.sendAsyncMessage( - "SessionStore:restoreTabContent", - options - ); - } + this._restoreTabContent(browser, options); }, - _restoreHistoryComplete(browser, data) { + _restoreHistoryComplete(browser) { let win = browser.ownerGlobal; let tab = win?.gBrowser.getTabForBrowser(browser); if (!tab) { @@ -7417,68 +7156,12 @@ var SessionStoreInternal = { delete options.tabData.storage; } - if (Services.appinfo.sessionHistoryInParent) { - this._restoreHistory(browser, options); - } else { - browser.messageManager.sendAsyncMessage( - "SessionStore:restoreHistory", - options - ); - } + this._restoreHistory(browser, options); if (browser && browser.frameLoader) { browser.frameLoader.requestEpochUpdate(options.epoch); } }, - - // Flush out session history state so that it can be used to restore the state - // into a new process in `finishTabRemotenessChange`. - // - // NOTE: This codepath is temporary while the Fission Session History rewrite - // is in process, and will be removed & replaced once that rewrite is - // complete. (bug 1645062) - async prepareToChangeRemoteness(aBrowser) { - aBrowser.messageManager.sendAsyncMessage( - "SessionStore:prepareForProcessChange" - ); - await lazy.TabStateFlusher.flush(aBrowser); - }, - - // Handle finishing the remoteness change for a tab by restoring session - // history state into it, and resuming the ongoing network load. - // - // NOTE: This codepath is temporary while the Fission Session History rewrite - // is in process, and will be removed & replaced once that rewrite is - // complete. (bug 1645062) - finishTabRemotenessChange(aTab, aSwitchId) { - let window = aTab.ownerGlobal; - if (!window || !window.__SSi || window.closed) { - return; - } - - let tabState = lazy.TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab)); - let options = { - restoreImmediately: true, - restoreContentReason: RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE, - isRemotenessUpdate: true, - loadArguments: { - redirectLoadSwitchId: aSwitchId, - // As we're resuming a load which has been redirected from another - // process, record the history index which is currently being requested. - // It has to be offset by 1 to get back to native history indices from - // SessionStore history indicies. - redirectHistoryIndex: tabState.requestedIndex - 1, - }, - }; - - // Need to reset restoring tabs. - if (TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser)) { - this._resetLocalTabRestoringState(aTab); - } - - // Restore the state into the tab. - this.restoreTab(aTab, tabState, options); - }, }; /** @@ -7689,7 +7372,7 @@ var DirtyWindows = { this._data.delete(window); }, - clear(window) { + clear(_window) { this._data = new WeakMap(); }, }; diff --git a/browser/components/sessionstore/StartupPerformance.sys.mjs b/browser/components/sessionstore/StartupPerformance.sys.mjs index a13333d9d1..c2b791609b 100644 --- a/browser/components/sessionstore/StartupPerformance.sys.mjs +++ b/browser/components/sessionstore/StartupPerformance.sys.mjs @@ -153,7 +153,7 @@ export var StartupPerformance = { }, COLLECT_RESULTS_AFTER_MS); }, - observe(subject, topic, details) { + observe(subject, topic) { try { switch (topic) { case "sessionstore-restoring-on-startup": diff --git a/browser/components/sessionstore/TabAttributes.sys.mjs b/browser/components/sessionstore/TabAttributes.sys.mjs index 1c7f54b6ab..ea53156d12 100644 --- a/browser/components/sessionstore/TabAttributes.sys.mjs +++ b/browser/components/sessionstore/TabAttributes.sys.mjs @@ -2,27 +2,13 @@ * 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/. */ -// We never want to directly read or write these attributes. -// 'image' should not be accessed directly but handled by using the -// gBrowser.getIcon()/setIcon() methods. -// 'muted' should not be accessed directly but handled by using the -// tab.linkedBrowser.audioMuted/toggleMuteAudio methods. -// 'pending' is used internal by sessionstore and managed accordingly. -const ATTRIBUTES_TO_SKIP = new Set([ - "image", - "muted", - "pending", - "skipbackgroundnotify", -]); +// Tab attributes which are persisted & restored by SessionStore. +const PERSISTED_ATTRIBUTES = ["customizemode"]; // A set of tab attributes to persist. We will read a given list of tab // attributes when collecting tab data and will re-set those attributes when // the given tab data is restored to a new tab. export var TabAttributes = Object.freeze({ - persist(name) { - return TabAttributesInternal.persist(name); - }, - get(tab) { return TabAttributesInternal.get(tab); }, @@ -33,21 +19,10 @@ export var TabAttributes = Object.freeze({ }); var TabAttributesInternal = { - _attrs: new Set(), - - persist(name) { - if (this._attrs.has(name) || ATTRIBUTES_TO_SKIP.has(name)) { - return false; - } - - this._attrs.add(name); - return true; - }, - get(tab) { let data = {}; - for (let name of this._attrs) { + for (let name of PERSISTED_ATTRIBUTES) { if (tab.hasAttribute(name)) { data[name] = tab.getAttribute(name); } @@ -57,15 +32,11 @@ var TabAttributesInternal = { }, set(tab, data = {}) { - // Clear attributes. - for (let name of this._attrs) { + // Clear & Set attributes. + for (let name of PERSISTED_ATTRIBUTES) { tab.removeAttribute(name); - } - - // Set attributes. - for (let [name, value] of Object.entries(data)) { - if (!ATTRIBUTES_TO_SKIP.has(name)) { - tab.setAttribute(name, value); + if (name in data) { + tab.setAttribute(name, data[name]); } } }, diff --git a/browser/components/sessionstore/TabStateFlusher.sys.mjs b/browser/components/sessionstore/TabStateFlusher.sys.mjs index e391abc970..ed7953e41e 100644 --- a/browser/components/sessionstore/TabStateFlusher.sys.mjs +++ b/browser/components/sessionstore/TabStateFlusher.sys.mjs @@ -2,11 +2,6 @@ * 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/. */ -const lazy = {}; -ChromeUtils.defineESModuleGetters(lazy, { - SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", -}); - /** * A module that enables async flushes. Updates from frame scripts are * throttled to be sent only once per second. If an action wants a tab's latest @@ -33,23 +28,6 @@ export var TabStateFlusher = Object.freeze({ }, /** - * Resolves the flush request with the given flush ID. - * - * @param browser (<xul:browser>) - * The browser for which the flush is being resolved. - * @param flushID (int) - * The ID of the flush that was sent to the browser. - * @param success (bool, optional) - * Whether or not the flush succeeded. - * @param message (string, optional) - * An error message that will be sent to the Console in the - * event that a flush failed. - */ - resolve(browser, flushID, success = true, message = "") { - TabStateFlusherInternal.resolve(browser, flushID, success, message); - }, - - /** * Resolves all active flush requests for a given browser. This should be * used when the content process crashed or the final update message was * seen. In those cases we can't guarantee to ever hear back from the frame @@ -69,9 +47,6 @@ export var TabStateFlusher = Object.freeze({ }); var TabStateFlusherInternal = { - // Stores the last request ID. - _lastRequestID: 0, - // A map storing all active requests per browser. A request is a // triple of a map containing all flush requests, a promise that // resolve when a request for a browser is canceled, and the @@ -79,7 +54,6 @@ var TabStateFlusherInternal = { _requests: new WeakMap(), initEntry(entry) { - entry.perBrowserRequests = new Map(); entry.cancelPromise = new Promise(resolve => { entry.cancel = resolve; }).then(result => { @@ -96,7 +70,6 @@ var TabStateFlusherInternal = { * all the latest data. */ flush(browser) { - let id = ++this._lastRequestID; let nativePromise = Promise.resolve(); if (browser && browser.frameLoader) { /* @@ -106,24 +79,6 @@ var TabStateFlusherInternal = { nativePromise = browser.frameLoader.requestTabStateFlush(); } - if (!Services.appinfo.sessionHistoryInParent) { - /* - In the event that we have to trigger a process switch and thus change - browser remoteness, session store needs to register and track the new - browser window loaded and to have message manager listener registered - ** before ** TabStateFlusher send "SessionStore:flush" message. This fixes - the race where we send the message before the message listener is - registered for it. - */ - lazy.SessionStore.ensureInitialized(browser.ownerGlobal); - - let mm = browser.messageManager; - mm.sendAsyncMessage("SessionStore:flush", { - id, - epoch: lazy.SessionStore.getCurrentEpoch(browser), - }); - } - // Retrieve active requests for given browser. let permanentKey = browser.permanentKey; let request = this._requests.get(permanentKey); @@ -134,22 +89,10 @@ var TabStateFlusherInternal = { this._requests.set(permanentKey, request); } - // Non-SHIP flushes resolve this after the "SessionStore:update" message. We - // don't use that message for SHIP, so it's fine to resolve the request - // immediately after the native promise resolves, since SessionStore will - // have processed all updates from this browser by that point. - let requestPromise = Promise.resolve(); - if (!Services.appinfo.sessionHistoryInParent) { - requestPromise = new Promise(resolve => { - // Store resolve() so that we can resolve the promise later. - request.perBrowserRequests.set(id, resolve); - }); - } - - return Promise.race([ - nativePromise.then(_ => requestPromise), - request.cancelPromise, - ]); + // It's fine to resolve the request immediately after the native promise + // resolves, since SessionStore will have processed all updates from this + // browser by that point. + return Promise.race([nativePromise, request.cancelPromise]); }, /** @@ -167,41 +110,6 @@ var TabStateFlusherInternal = { }, /** - * Resolves the flush request with the given flush ID. - * - * @param browser (<xul:browser>) - * The browser for which the flush is being resolved. - * @param flushID (int) - * The ID of the flush that was sent to the browser. - * @param success (bool, optional) - * Whether or not the flush succeeded. - * @param message (string, optional) - * An error message that will be sent to the Console in the - * event that a flush failed. - */ - resolve(browser, flushID, success = true, message = "") { - // Nothing to do if there are no pending flushes for the given browser. - if (!this._requests.has(browser.permanentKey)) { - return; - } - - // Retrieve active requests for given browser. - let { perBrowserRequests } = this._requests.get(browser.permanentKey); - if (!perBrowserRequests.has(flushID)) { - return; - } - - if (!success) { - console.error("Failed to flush browser: ", message); - } - - // Resolve the request with the given id. - let resolve = perBrowserRequests.get(flushID); - perBrowserRequests.delete(flushID); - resolve(success); - }, - - /** * Resolves all active flush requests for a given browser. This should be * used when the content process crashed or the final update message was * seen. In those cases we can't guarantee to ever hear back from the frame diff --git a/browser/components/sessionstore/content/aboutSessionRestore.js b/browser/components/sessionstore/content/aboutSessionRestore.js index 51bed7c51b..2dfa45d40f 100644 --- a/browser/components/sessionstore/content/aboutSessionRestore.js +++ b/browser/components/sessionstore/content/aboutSessionRestore.js @@ -204,7 +204,7 @@ function startNewSession() { ), }); } else { - getBrowserWindow().BrowserHome(); + getBrowserWindow().BrowserCommands.home(); } } @@ -325,31 +325,31 @@ var treeView = { setTree(treeBox) { this.treeBox = treeBox; }, - getCellText(idx, column) { + getCellText(idx) { return gTreeData[idx].label; }, isContainer(idx) { return "open" in gTreeData[idx]; }, - getCellValue(idx, column) { + getCellValue(idx) { return gTreeData[idx].checked; }, isContainerOpen(idx) { return gTreeData[idx].open; }, - isContainerEmpty(idx) { + isContainerEmpty() { return false; }, - isSeparator(idx) { + isSeparator() { return false; }, isSorted() { return false; }, - isEditable(idx, column) { + isEditable() { return false; }, - canDrop(idx, orientation, dt) { + canDrop() { return false; }, getLevel(idx) { @@ -438,10 +438,10 @@ var treeView = { return null; }, - cycleHeader(column) {}, - cycleCell(idx, column) {}, + cycleHeader() {}, + cycleCell() {}, selectionChanged() {}, - getColumnProperties(column) { + getColumnProperties() { return ""; }, }; diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js deleted file mode 100644 index a4bdea0bdc..0000000000 --- a/browser/components/sessionstore/content/content-sessionStore.js +++ /dev/null @@ -1,13 +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/. */ - -/* eslint-env mozilla/frame-script */ - -"use strict"; - -const { ContentSessionStore } = ChromeUtils.importESModule( - "resource:///modules/sessionstore/ContentSessionStore.sys.mjs" -); - -void new ContentSessionStore(this); diff --git a/browser/components/sessionstore/jar.mn b/browser/components/sessionstore/jar.mn index 7e5bc07dc6..b31a4fb351 100644 --- a/browser/components/sessionstore/jar.mn +++ b/browser/components/sessionstore/jar.mn @@ -5,4 +5,3 @@ browser.jar: * content/browser/aboutSessionRestore.xhtml (content/aboutSessionRestore.xhtml) content/browser/aboutSessionRestore.js (content/aboutSessionRestore.js) - content/browser/content-sessionStore.js (content/content-sessionStore.js) diff --git a/browser/components/sessionstore/moz.build b/browser/components/sessionstore/moz.build index 1536826733..cd3a0ad6fc 100644 --- a/browser/components/sessionstore/moz.build +++ b/browser/components/sessionstore/moz.build @@ -5,14 +5,12 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"] -BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] +BROWSER_CHROME_MANIFESTS += ["test/browser.toml", "test/browser_oldformat.toml"] MARIONETTE_MANIFESTS += ["test/marionette/manifest.toml"] JAR_MANIFESTS += ["jar.mn"] EXTRA_JS_MODULES.sessionstore = [ - "ContentRestore.sys.mjs", - "ContentSessionStore.sys.mjs", "GlobalState.sys.mjs", "RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs", "RunState.sys.mjs", diff --git a/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs b/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs index dd2885cee4..eecb1240e2 100644 --- a/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs +++ b/browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs @@ -100,7 +100,7 @@ export var SessionStoreTestUtils = { expectedTabsRestored = aState.windows.length; } - function onSSTabRestored(aEvent) { + function onSSTabRestored() { if (++tabsRestored == expectedTabsRestored) { // Remove the event listener from each window windows.forEach(function (win) { @@ -118,7 +118,7 @@ export var SessionStoreTestUtils = { // Used to add our listener to further windows so we can catch SSTabRestored // coming from them when creating a multi-window state. - function windowObserver(aSubject, aTopic, aData) { + function windowObserver(aSubject, aTopic) { if (aTopic == "domwindowopened") { let newWindow = aSubject; newWindow.addEventListener( diff --git a/browser/components/sessionstore/test/browser.toml b/browser/components/sessionstore/test/browser.toml index 26fb4b4550..7d5b407d22 100644 --- a/browser/components/sessionstore/test/browser.toml +++ b/browser/components/sessionstore/test/browser.toml @@ -22,30 +22,11 @@ support-files = [ "browser_scrollPositions_readerModeArticle.html", "browser_sessionStorage.html", "browser_speculative_connect.html", - "browser_248970_b_sample.html", - "browser_339445_sample.html", - "browser_423132_sample.html", - "browser_447951_sample.html", - "browser_454908_sample.html", - "browser_456342_sample.xhtml", - "browser_463205_sample.html", - "browser_463206_sample.html", - "browser_466937_sample.html", - "browser_485482_sample.html", - "browser_637020_slow.sjs", - "browser_662743_sample.html", - "browser_739531_sample.html", - "browser_739531_frame.html", - "browser_911547_sample.html", - "browser_911547_sample.html^headers^", "coopHeaderCommon.sjs", "restore_redirect_http.html", "restore_redirect_http.html^headers^", "restore_redirect_js.html", "restore_redirect_target.html", - "browser_1234021_page.html", - "browser_1284886_suspend_tab.html", - "browser_1284886_suspend_tab_2.html", "empty.html", "coop_coep.html", "coop_coep.html^headers^", @@ -58,248 +39,6 @@ prefs = [ "browser.sessionstore.closedTabsFromClosedWindows=true", ] -#NB: the following are disabled -# browser_464620_a.html -# browser_464620_b.html -# browser_464620_xd.html - -#disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html -#disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html -#disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html - -["browser_1234021.js"] - -["browser_1284886_suspend_tab.js"] - -["browser_1446343-windowsize.js"] -skip-if = ["os == 'linux'"] # Bug 1600180 - -["browser_248970_b_perwindowpb.js"] -# Disabled because of leaks. -# Re-enabling and rewriting this test is tracked in bug 936919. -skip-if = ["true"] - -["browser_339445.js"] - -["browser_345898.js"] - -["browser_350525.js"] - -["browser_354894_perwindowpb.js"] - -["browser_367052.js"] - -["browser_393716.js"] -skip-if = ["debug"] # Bug 1507747 - -["browser_394759_basic.js"] -# Disabled for intermittent failures, bug 944372. -skip-if = ["true"] - -["browser_394759_behavior.js"] -https_first_disabled = true - -["browser_394759_perwindowpb.js"] - -["browser_394759_purge.js"] - -["browser_423132.js"] - -["browser_447951.js"] - -["browser_454908.js"] - -["browser_456342.js"] - -["browser_461634.js"] - -["browser_463205.js"] - -["browser_463206.js"] - -["browser_464199.js"] -# Disabled for frequent intermittent failures - -["browser_464620_a.js"] -skip-if = ["true"] - -["browser_464620_b.js"] -skip-if = ["true"] - -["browser_465215.js"] - -["browser_465223.js"] - -["browser_466937.js"] - -["browser_467409-backslashplosion.js"] - -["browser_477657.js"] -skip-if = ["os == 'linux' && os_version == '18.04'"] # bug 1610668 for ubuntu 18.04 - -["browser_480893.js"] - -["browser_485482.js"] - -["browser_485563.js"] - -["browser_490040.js"] - -["browser_491168.js"] - -["browser_491577.js"] -skip-if = [ - "verify && debug && os == 'mac'", - "verify && debug && os == 'win'", -] - -["browser_495495.js"] - -["browser_500328.js"] - -["browser_514751.js"] - -["browser_522375.js"] - -["browser_522545.js"] -skip-if = ["true"] # Bug 1380968 - -["browser_524745.js"] -skip-if = [ - "win10_2009 && !ccov", # Bug 1418627 - "os == 'linux'", # Bug 1803187 -] - -["browser_528776.js"] - -["browser_579868.js"] - -["browser_579879.js"] -skip-if = ["os == 'linux' && (debug || asan)"] # Bug 1234404 - -["browser_581937.js"] - -["browser_586068-apptabs.js"] - -["browser_586068-apptabs_ondemand.js"] -skip-if = ["verify && (os == 'mac' || os == 'win')"] - -["browser_586068-browser_state_interrupted.js"] - -["browser_586068-cascade.js"] - -["browser_586068-multi_window.js"] - -["browser_586068-reload.js"] -https_first_disabled = true - -["browser_586068-select.js"] - -["browser_586068-window_state.js"] - -["browser_586068-window_state_override.js"] - -["browser_586147.js"] - -["browser_588426.js"] - -["browser_590268.js"] - -["browser_590563.js"] - -["browser_595601-restore_hidden.js"] - -["browser_597071.js"] -skip-if = ["true"] # Needs to be rewritten as Marionette test, bug 995916 - -["browser_600545.js"] - -["browser_601955.js"] - -["browser_607016.js"] - -["browser_615394-SSWindowState_events_duplicateTab.js"] - -["browser_615394-SSWindowState_events_setBrowserState.js"] -skip-if = ["verify && debug && os == 'mac'"] - -["browser_615394-SSWindowState_events_setTabState.js"] - -["browser_615394-SSWindowState_events_setWindowState.js"] -https_first_disabled = true - -["browser_615394-SSWindowState_events_undoCloseTab.js"] - -["browser_615394-SSWindowState_events_undoCloseWindow.js"] -skip-if = [ - "os == 'win' && !debug", # Bug 1572554 - "os == 'linux'", # Bug 1572554 -] - -["browser_618151.js"] - -["browser_623779.js"] - -["browser_624727.js"] - -["browser_625016.js"] -skip-if = [ - "os == 'mac'", # Disabled on OS X: - "os == 'linux'", # linux, Bug 1348583 - "os == 'win' && debug", # Bug 1430977 -] - -["browser_628270.js"] - -["browser_635418.js"] - -["browser_636279.js"] - -["browser_637020.js"] - -["browser_645428.js"] - -["browser_659591.js"] - -["browser_662743.js"] - -["browser_662812.js"] -skip-if = ["verify"] - -["browser_665702-state_session.js"] - -["browser_682507.js"] - -["browser_687710.js"] - -["browser_687710_2.js"] -https_first_disabled = true - -["browser_694378.js"] - -["browser_701377.js"] -skip-if = [ - "verify && debug && os == 'win'", - "verify && debug && os == 'mac'", -] - -["browser_705597.js"] - -["browser_707862.js"] - -["browser_739531.js"] - -["browser_739805.js"] - -["browser_819510_perwindowpb.js"] -skip-if = ["true"] # Bug 1284312, Bug 1341980, bug 1381451 - -["browser_906076_lazy_tabs.js"] -https_first_disabled = true -skip-if = ["os == 'linux' && os_version == '18.04'"] # bug 1446464 - -["browser_911547.js"] - ["browser_aboutPrivateBrowsing.js"] ["browser_aboutSessionRestore.js"] @@ -316,7 +55,6 @@ support-files = ["file_async_flushes.html"] run-if = ["crashreporter"] ["browser_async_remove_tab.js"] -skip-if = ["!sessionHistoryInParent"] ["browser_async_window_flushing.js"] https_first_disabled = true @@ -393,6 +131,7 @@ https_first_disabled = true skip-if = ["verify && debug"] ["browser_formdata_cc.js"] +skip-if = ["asan"] # test runs too long ["browser_formdata_face.js"] @@ -466,12 +205,12 @@ skip-if = [ ["browser_privatetabs.js"] ["browser_purge_shistory.js"] -skip-if = ["!sessionHistoryInParent"] # Bug 1271024 ["browser_remoteness_flip_on_restore.js"] ["browser_reopen_all_windows.js"] https_first_disabled = true +skip-if = ["asan"] # high memory ["browser_replace_load.js"] skip-if = ["true"] # Bug 1646894 @@ -516,9 +255,6 @@ skip-if = [ ["browser_scrollPositionsReaderMode.js"] -["browser_send_async_message_oom.js"] -skip-if = ["sessionHistoryInParent"] # Tests that the frame script OOMs, which is unused when SHIP is enabled. - ["browser_sessionHistory.js"] https_first_disabled = true support-files = ["file_sessionHistory_hashchange.html"] diff --git a/browser/components/sessionstore/test/browser_354894_perwindowpb.js b/browser/components/sessionstore/test/browser_354894_perwindowpb.js index 90368536dc..30a065c1af 100644 --- a/browser/components/sessionstore/test/browser_354894_perwindowpb.js +++ b/browser/components/sessionstore/test/browser_354894_perwindowpb.js @@ -21,7 +21,7 @@ * not enabled on that platform (platform shim; the application is kept running * although there are no windows left) * @note There is a difference when closing a browser window with - * BrowserTryToCloseWindow() as opposed to close(). The former will make + * BrowserCommands.tryToCloseWindow() as opposed to close(). The former will make * nsSessionStore restore a window next time it gets a chance and will post * notifications. The latter won't. */ @@ -133,7 +133,7 @@ let setupTest = async function (options, testFunction) { * Helper: Will observe and handle the notifications for us */ let hitCount = 0; - function observer(aCancel, aTopic, aData) { + function observer(aCancel, aTopic) { // count so that we later may compare observing[aTopic]++; @@ -182,7 +182,7 @@ function injectTestTabs(win) { } /** - * Attempts to close a window via BrowserTryToCloseWindow so that + * Attempts to close a window via BrowserCommands.tryToCloseWindow so that * we get the browser-lastwindow-close-requested and * browser-lastwindow-close-granted observer notifications. * @@ -195,7 +195,7 @@ function injectTestTabs(win) { function closeWindowForRestoration(win) { return new Promise(resolve => { let closePromise = BrowserTestUtils.windowClosed(win); - win.BrowserTryToCloseWindow(); + win.BrowserCommands.tryToCloseWindow(); if (!win.closed) { resolve(false); return; @@ -415,7 +415,7 @@ add_task(async function test_open_close_restore_from_popup() { return; } - await setupTest({}, async function (newWin, obs) { + await setupTest({}, async function (newWin) { let newWin2 = await promiseNewWindowLoaded(); await injectTestTabs(newWin2); diff --git a/browser/components/sessionstore/test/browser_394759_basic.js b/browser/components/sessionstore/test/browser_394759_basic.js index 62d5c40e17..cc1c335165 100644 --- a/browser/components/sessionstore/test/browser_394759_basic.js +++ b/browser/components/sessionstore/test/browser_394759_basic.js @@ -74,7 +74,7 @@ function test() { let expectedTabs = data[0].tabs.length; newWin2.addEventListener( "SSTabRestored", - function sstabrestoredListener(aEvent) { + function sstabrestoredListener() { ++restoredTabs; info("Restored tab " + restoredTabs + "/" + expectedTabs); if (restoredTabs < expectedTabs) { diff --git a/browser/components/sessionstore/test/browser_394759_behavior.js b/browser/components/sessionstore/test/browser_394759_behavior.js index ee4b121e84..01217f86c9 100644 --- a/browser/components/sessionstore/test/browser_394759_behavior.js +++ b/browser/components/sessionstore/test/browser_394759_behavior.js @@ -34,7 +34,7 @@ function testWindows(windowsToOpen, expectedResults) { } let closedWindowData = ss.getClosedWindowData(); - let numPopups = closedWindowData.filter(function (el, i, arr) { + let numPopups = closedWindowData.filter(function (el) { return el.isPopup; }).length; let numNormal = ss.getClosedWindowCount() - numPopups; @@ -50,7 +50,7 @@ function testWindows(windowsToOpen, expectedResults) { is( numNormal, oResults.normal, - "There were " + oResults.normal + " normal windows to repoen" + "There were " + oResults.normal + " normal windows to reopen" ); })(); } @@ -63,14 +63,15 @@ add_task(async function test_closed_window_states() { let windowsToOpen = [ { isPopup: false }, - { isPopup: false }, + { isPopup: true }, + { isPopup: true }, { isPopup: true }, { isPopup: true }, { isPopup: true }, ]; let expectedResults = { - mac: { popup: 3, normal: 0 }, - other: { popup: 3, normal: 1 }, + mac: { popup: 5, normal: 0 }, + other: { popup: 5, normal: 1 }, }; await testWindows(windowsToOpen, expectedResults); @@ -81,10 +82,11 @@ add_task(async function test_closed_window_states() { { isPopup: false }, { isPopup: false }, { isPopup: false }, + { isPopup: false }, ]; let expectedResults2 = { - mac: { popup: 0, normal: 3 }, - other: { popup: 0, normal: 3 }, + mac: { popup: 0, normal: 5 }, + other: { popup: 0, normal: 5 }, }; await testWindows(windowsToOpen2, expectedResults2); diff --git a/browser/components/sessionstore/test/browser_394759_purge.js b/browser/components/sessionstore/test/browser_394759_purge.js index e5218c9936..ea75d6e4b2 100644 --- a/browser/components/sessionstore/test/browser_394759_purge.js +++ b/browser/components/sessionstore/test/browser_394759_purge.js @@ -9,7 +9,7 @@ let { ForgetAboutSite } = ChromeUtils.importESModule( function promiseClearHistory() { return new Promise(resolve => { let observer = { - observe(aSubject, aTopic, aData) { + observe() { Services.obs.removeObserver( this, "browser:purge-session-history-for-domain" diff --git a/browser/components/sessionstore/test/browser_459906.js b/browser/components/sessionstore/test/browser_459906.js index 6827f6ad1d..5a0c1aeea3 100644 --- a/browser/components/sessionstore/test/browser_459906.js +++ b/browser/components/sessionstore/test/browser_459906.js @@ -17,7 +17,7 @@ function test() { let tab = BrowserTestUtils.addTab(gBrowser, testURL); tab.linkedBrowser.addEventListener( "load", - function listener(aEvent) { + function listener() { // wait for all frames to load completely if (frameCount++ < 2) { return; @@ -31,7 +31,7 @@ function test() { let tab2 = gBrowser.duplicateTab(tab); tab2.linkedBrowser.addEventListener( "load", - function loadListener(eventTab2) { + function loadListener() { // wait for all frames to load (and reload!) completely if (frameCount++ < 2) { return; diff --git a/browser/components/sessionstore/test/browser_461743.js b/browser/components/sessionstore/test/browser_461743.js index fd4501b5ac..a27ccc7721 100644 --- a/browser/components/sessionstore/test/browser_461743.js +++ b/browser/components/sessionstore/test/browser_461743.js @@ -24,7 +24,7 @@ function test() { let tab2 = gBrowser.duplicateTab(tab); tab2.linkedBrowser.addEventListener( "461743", - function listener(eventTab2) { + function listener() { tab2.linkedBrowser.removeEventListener("461743", listener, true); is(aEvent.data, "done", "XSS injection was attempted"); diff --git a/browser/components/sessionstore/test/browser_464199.js b/browser/components/sessionstore/test/browser_464199.js index 4ac8fba1a5..98a17c4955 100644 --- a/browser/components/sessionstore/test/browser_464199.js +++ b/browser/components/sessionstore/test/browser_464199.js @@ -9,7 +9,7 @@ let { ForgetAboutSite } = ChromeUtils.importESModule( function promiseClearHistory() { return new Promise(resolve => { let observer = { - observe(aSubject, aTopic, aData) { + observe() { Services.obs.removeObserver( this, "browser:purge-session-history-for-domain" diff --git a/browser/components/sessionstore/test/browser_464620_a.js b/browser/components/sessionstore/test/browser_464620_a.js index 9052d7bec0..6a3b56f767 100644 --- a/browser/components/sessionstore/test/browser_464620_a.js +++ b/browser/components/sessionstore/test/browser_464620_a.js @@ -27,7 +27,7 @@ function test() { let tab2 = gBrowser.duplicateTab(tab); tab2.linkedBrowser.addEventListener( "464620_a", - function listener(eventTab2) { + function listener() { tab2.linkedBrowser.removeEventListener("464620_a", listener, true); is(aEvent.data, "done", "XSS injection was attempted"); diff --git a/browser/components/sessionstore/test/browser_464620_b.js b/browser/components/sessionstore/test/browser_464620_b.js index 005bb4cc27..3e2b46d685 100644 --- a/browser/components/sessionstore/test/browser_464620_b.js +++ b/browser/components/sessionstore/test/browser_464620_b.js @@ -27,7 +27,7 @@ function test() { let tab2 = gBrowser.duplicateTab(tab); tab2.linkedBrowser.addEventListener( "464620_b", - function listener(eventTab2) { + function listener() { tab2.linkedBrowser.removeEventListener("464620_b", listener, true); is(aEvent.data, "done", "XSS injection was attempted"); diff --git a/browser/components/sessionstore/test/browser_526613.js b/browser/components/sessionstore/test/browser_526613.js index ba3f03ef32..784febd3d5 100644 --- a/browser/components/sessionstore/test/browser_526613.js +++ b/browser/components/sessionstore/test/browser_526613.js @@ -45,7 +45,7 @@ function test() { }; let pass = 1; - function observer(aSubject, aTopic, aData) { + function observer(aSubject, aTopic) { is( aTopic, "sessionstore-browser-state-restored", diff --git a/browser/components/sessionstore/test/browser_580512.js b/browser/components/sessionstore/test/browser_580512.js index 1dfd696277..e27dc61ba3 100644 --- a/browser/components/sessionstore/test/browser_580512.js +++ b/browser/components/sessionstore/test/browser_580512.js @@ -32,10 +32,10 @@ function closeFirstWin(win) { win.gBrowser.pinTab(win.gBrowser.tabs[1]); let winClosed = BrowserTestUtils.windowClosed(win); - // We need to call BrowserTryToCloseWindow in order to trigger + // We need to call BrowserCommands.tryToCloseWindow in order to trigger // the machinery that chooses whether or not to save the session // for the last window. - win.BrowserTryToCloseWindow(); + win.BrowserCommands.tryToCloseWindow(); ok(win.closed, "window closed"); winClosed.then(() => { @@ -88,7 +88,7 @@ function openWinWithCb(cb, argURIs, expectedURIs) { var expectedLoads = expectedURIs.length; win.gBrowser.addTabsProgressListener({ - onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, _aStatus) { if ( aRequest && aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && diff --git a/browser/components/sessionstore/test/browser_586068-apptabs.js b/browser/components/sessionstore/test/browser_586068-apptabs.js index b2f92f760c..24878ba267 100644 --- a/browser/components/sessionstore/test/browser_586068-apptabs.js +++ b/browser/components/sessionstore/test/browser_586068-apptabs.js @@ -69,12 +69,7 @@ add_task(async function test() { let loadCount = 0; let promiseRestoringTabs = new Promise(resolve => { - gProgressListener.setCallback(function ( - aBrowser, - aNeedRestore, - aRestoring, - aRestored - ) { + gProgressListener.setCallback(function (aBrowser) { loadCount++; // We'll make sure that the loads we get come from pinned tabs or the diff --git a/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js index b729555ff1..6ef18e2b3a 100644 --- a/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js +++ b/browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js @@ -147,12 +147,7 @@ add_task(async function test() { let loadCount = 0; let promiseRestoringTabs = new Promise(resolve => { - gProgressListener.setCallback(function ( - aBrowser, - aNeedRestore, - aRestoring, - aRestored - ) { + gProgressListener.setCallback(function (aBrowser, aNeedRestore) { loadCount++; if ( @@ -188,7 +183,7 @@ add_task(async function test() { }); // We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened - Services.ww.registerNotification(function observer(aSubject, aTopic, aData) { + Services.ww.registerNotification(function observer(aSubject, aTopic) { if (aTopic == "domwindowopened") { let win = aSubject; win.addEventListener( diff --git a/browser/components/sessionstore/test/browser_586068-multi_window.js b/browser/components/sessionstore/test/browser_586068-multi_window.js index bf5d839812..352c5bcefb 100644 --- a/browser/components/sessionstore/test/browser_586068-multi_window.js +++ b/browser/components/sessionstore/test/browser_586068-multi_window.js @@ -72,12 +72,7 @@ add_task(async function test() { let loadCount = 0; let promiseRestoringTabs = new Promise(resolve => { - gProgressListener.setCallback(function ( - aBrowser, - aNeedRestore, - aRestoring, - aRestored - ) { + gProgressListener.setCallback(function (aBrowser, aNeedRestore) { if (++loadCount == numTabs) { // We don't actually care about load order in this test, just that they all // do load. @@ -91,7 +86,7 @@ add_task(async function test() { }); // We also want to catch the 2nd window, so we need to observe domwindowopened - Services.ww.registerNotification(function observer(aSubject, aTopic, aData) { + Services.ww.registerNotification(function observer(aSubject, aTopic) { if (aTopic == "domwindowopened") { let win = aSubject; win.addEventListener( diff --git a/browser/components/sessionstore/test/browser_586068-window_state.js b/browser/components/sessionstore/test/browser_586068-window_state.js index 69c3742a66..25066a2db4 100644 --- a/browser/components/sessionstore/test/browser_586068-window_state.js +++ b/browser/components/sessionstore/test/browser_586068-window_state.js @@ -82,12 +82,7 @@ add_task(async function test() { let loadCount = 0; let promiseRestoringTabs = new Promise(resolve => { - gProgressListener.setCallback(function ( - aBrowser, - aNeedRestore, - aRestoring, - aRestored - ) { + gProgressListener.setCallback(function (aBrowser, aNeedRestore) { // When loadCount == 2, we'll also restore state2 into the window if (++loadCount == 2) { ss.setWindowState(window, JSON.stringify(state2), false); diff --git a/browser/components/sessionstore/test/browser_586068-window_state_override.js b/browser/components/sessionstore/test/browser_586068-window_state_override.js index 8a6eac6de2..eb3d2c709b 100644 --- a/browser/components/sessionstore/test/browser_586068-window_state_override.js +++ b/browser/components/sessionstore/test/browser_586068-window_state_override.js @@ -82,12 +82,7 @@ add_task(async function test() { let loadCount = 0; let promiseRestoringTabs = new Promise(resolve => { - gProgressListener.setCallback(function ( - aBrowser, - aNeedRestore, - aRestoring, - aRestored - ) { + gProgressListener.setCallback(function (aBrowser, aNeedRestore) { // When loadCount == 2, we'll also restore state2 into the window if (++loadCount == 2) { executeSoon(() => diff --git a/browser/components/sessionstore/test/browser_589246.js b/browser/components/sessionstore/test/browser_589246.js index 2fd92b2b82..34d9dc97a8 100644 --- a/browser/components/sessionstore/test/browser_589246.js +++ b/browser/components/sessionstore/test/browser_589246.js @@ -164,7 +164,7 @@ function setupForTest(aConditions) { ss.setBrowserState(JSON.stringify(testState)); } -function onStateRestored(aSubject, aTopic, aData) { +function onStateRestored() { info("test #" + testNum + ": onStateRestored"); Services.obs.removeObserver( onStateRestored, @@ -183,7 +183,7 @@ function onStateRestored(aSubject, aTopic, aData) { ); newWin.addEventListener( "load", - function (aEvent) { + function () { promiseBrowserLoaded(newWin.gBrowser.selectedBrowser).then(() => { // pin this tab if (shouldPinTab) { @@ -216,12 +216,12 @@ function onStateRestored(aSubject, aTopic, aData) { newWin.gBrowser.removeTab(newTab); newWin.gBrowser.removeTab(newTab2); } - newWin.BrowserTryToCloseWindow(); + newWin.BrowserCommands.tryToCloseWindow(); }, { capture: true, once: true } ); } else { - newWin.BrowserTryToCloseWindow(); + newWin.BrowserCommands.tryToCloseWindow(); } }); }, @@ -230,7 +230,7 @@ function onStateRestored(aSubject, aTopic, aData) { } // This will be called before the window is actually closed -function onLastWindowClosed(aSubject, aTopic, aData) { +function onLastWindowClosed() { info("test #" + testNum + ": onLastWindowClosed"); Services.obs.removeObserver( onLastWindowClosed, @@ -261,7 +261,7 @@ function onWindowUnloaded() { ); newWin.addEventListener( "load", - function (aEvent) { + function () { newWin.gBrowser.selectedBrowser.addEventListener( "load", function () { diff --git a/browser/components/sessionstore/test/browser_590268.js b/browser/components/sessionstore/test/browser_590268.js index cde1a1cafa..eb1940e35d 100644 --- a/browser/components/sessionstore/test/browser_590268.js +++ b/browser/components/sessionstore/test/browser_590268.js @@ -52,7 +52,7 @@ function test() { } } - function onSSTabRestored(aEvent) { + function onSSTabRestored() { if (++restoredTabsCount < NUM_TABS) { return; } diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js index b3ad6d240a..b2f7692b7c 100644 --- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js +++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js @@ -29,11 +29,11 @@ function test_duplicateTab() { // We'll look to make sure this value is on the duplicated tab ss.setCustomTabValue(tab, "foo", "bar"); - function onSSWindowStateBusy(aEvent) { + function onSSWindowStateBusy() { busyEventCount++; } - function onSSWindowStateReady(aEvent) { + function onSSWindowStateReady() { newTab = gBrowser.tabs[2]; readyEventCount++; is(ss.getCustomTabValue(newTab, "foo"), "bar"); diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js index 4dfcbc844d..fbca3301e6 100644 --- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js +++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js @@ -84,7 +84,7 @@ function test() { // waitForBrowserState does it's own observing for windows, but doesn't attach // the listeners we want here, so do it ourselves. let newWindow; - function windowObserver(aSubject, aTopic, aData) { + function windowObserver(aSubject, aTopic) { if (aTopic == "domwindowopened") { Services.ww.unregisterNotification(windowObserver); diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js index a76a8b3dd5..4b0c256388 100644 --- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js +++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js @@ -29,17 +29,17 @@ function test_setTabState() { let busyEventCount = 0; let readyEventCount = 0; - function onSSWindowStateBusy(aEvent) { + function onSSWindowStateBusy() { busyEventCount++; } - function onSSWindowStateReady(aEvent) { + function onSSWindowStateReady() { readyEventCount++; is(ss.getCustomTabValue(tab, "foo"), "bar"); ss.setCustomTabValue(tab, "baz", "qux"); } - function onSSTabRestoring(aEvent) { + function onSSTabRestoring() { is(busyEventCount, 1); is(readyEventCount, 1); is(ss.getCustomTabValue(tab, "baz"), "qux"); diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js index c9d4bd00f5..daa40bd75a 100644 --- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js +++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js @@ -29,17 +29,17 @@ function test() { readyEventCount = 0, tabRestoredCount = 0; - function onSSWindowStateBusy(aEvent) { + function onSSWindowStateBusy() { busyEventCount++; } - function onSSWindowStateReady(aEvent) { + function onSSWindowStateReady() { readyEventCount++; is(ss.getCustomTabValue(gBrowser.tabs[0], "foo"), "bar"); is(ss.getCustomTabValue(gBrowser.tabs[1], "baz"), "qux"); } - function onSSTabRestored(aEvent) { + function onSSTabRestored() { if (++tabRestoredCount < 2) { return; } diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js index 345bba516c..b5d5af2835 100644 --- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js +++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js @@ -24,11 +24,11 @@ add_task(async function test_undoCloseTab() { ss.setCustomTabValue(tab, "foo", "bar"); - function onSSWindowStateBusy(aEvent) { + function onSSWindowStateBusy() { busyEventCount++; } - function onSSWindowStateReady(aEvent) { + function onSSWindowStateReady() { Assert.equal(gBrowser.tabs.length, 2, "Should only have 2 tabs"); lastTab = gBrowser.tabs[1]; readyEventCount++; diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js index 0a5b07da29..7483583e5a 100644 --- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js +++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js @@ -71,7 +71,7 @@ function test() { let newWindow, reopenedWindow; - function firstWindowObserver(aSubject, aTopic, aData) { + function firstWindowObserver(aSubject, aTopic) { if (aTopic == "domwindowopened") { newWindow = aSubject; Services.ww.unregisterNotification(firstWindowObserver); @@ -107,15 +107,15 @@ function test() { readyEventCount = 0, tabRestoredCount = 0; // These will listen to the reopened closed window... - function onSSWindowStateBusy(aEvent) { + function onSSWindowStateBusy() { busyEventCount++; } - function onSSWindowStateReady(aEvent) { + function onSSWindowStateReady() { readyEventCount++; } - function onSSTabRestored(aEvent) { + function onSSTabRestored() { if (++tabRestoredCount < 4) { return; } diff --git a/browser/components/sessionstore/test/browser_618151.js b/browser/components/sessionstore/test/browser_618151.js index c38a349818..f3c44d1e88 100644 --- a/browser/components/sessionstore/test/browser_618151.js +++ b/browser/components/sessionstore/test/browser_618151.js @@ -46,7 +46,7 @@ function runNextTest() { } function test_setup() { - function onSSTabRestored(aEvent) { + function onSSTabRestored() { gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored); runNextTest(); } diff --git a/browser/components/sessionstore/test/browser_636279.js b/browser/components/sessionstore/test/browser_636279.js index 3b71fcbb4c..4842f145b2 100644 --- a/browser/components/sessionstore/test/browser_636279.js +++ b/browser/components/sessionstore/test/browser_636279.js @@ -129,7 +129,7 @@ var TabsProgressListener = { delete this.callback; }, - observe(browser, topic, data) { + observe(browser) { TabsProgressListener.onRestored(browser); }, diff --git a/browser/components/sessionstore/test/browser_645428.js b/browser/components/sessionstore/test/browser_645428.js index bbb3b1b299..3916c44a7e 100644 --- a/browser/components/sessionstore/test/browser_645428.js +++ b/browser/components/sessionstore/test/browser_645428.js @@ -6,7 +6,7 @@ const NOTIFICATION = "sessionstore-browser-state-restored"; function test() { waitForExplicitFinish(); - function observe(subject, topic, data) { + function observe(subject, topic) { if (NOTIFICATION == topic) { finish(); ok(true, "TOPIC received"); diff --git a/browser/components/sessionstore/test/browser_687710_2.js b/browser/components/sessionstore/test/browser_687710_2.js index 81d3c55379..190b5a718a 100644 --- a/browser/components/sessionstore/test/browser_687710_2.js +++ b/browser/components/sessionstore/test/browser_687710_2.js @@ -38,61 +38,31 @@ var state = { add_task(async function test() { let tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); await promiseTabState(tab, state); - if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { - await SpecialPowers.spawn(tab.linkedBrowser, [], function () { - function compareEntries(i, j, history) { - let e1 = history.getEntryAtIndex(i); - let e2 = history.getEntryAtIndex(j); - ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`); - is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`); + function compareEntries(i, j, history) { + let e1 = history.getEntryAtIndex(i); + let e2 = history.getEntryAtIndex(j); - for (let c = 0; c < e1.childCount; c++) { - let c1 = e1.GetChildAt(c); - let c2 = e2.GetChildAt(c); + ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`); + is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`); - ok( - c1.sharesDocumentWith(c2), - `Cousins should share documents. (${i}, ${j}, ${c})` - ); - } - } + for (let c = 0; c < e1.childCount; c++) { + let c1 = e1.GetChildAt(c); + let c2 = e2.GetChildAt(c); - let history = docShell.browsingContext.childSessionHistory.legacySHistory; - - is(history.count, 2, "history.count"); - for (let i = 0; i < history.count; i++) { - for (let j = 0; j < history.count; j++) { - compareEntries(i, j, history); - } - } - }); - } else { - function compareEntries(i, j, history) { - let e1 = history.getEntryAtIndex(i); - let e2 = history.getEntryAtIndex(j); - - ok(e1.sharesDocumentWith(e2), `${i} should share doc with ${j}`); - is(e1.childCount, e2.childCount, `Child count mismatch (${i}, ${j})`); - - for (let c = 0; c < e1.childCount; c++) { - let c1 = e1.GetChildAt(c); - let c2 = e2.GetChildAt(c); - - ok( - c1.sharesDocumentWith(c2), - `Cousins should share documents. (${i}, ${j}, ${c})` - ); - } + ok( + c1.sharesDocumentWith(c2), + `Cousins should share documents. (${i}, ${j}, ${c})` + ); } + } - let history = tab.linkedBrowser.browsingContext.sessionHistory; + let history = tab.linkedBrowser.browsingContext.sessionHistory; - is(history.count, 2, "history.count"); - for (let i = 0; i < history.count; i++) { - for (let j = 0; j < history.count; j++) { - compareEntries(i, j, history); - } + is(history.count, 2, "history.count"); + for (let i = 0; i < history.count; i++) { + for (let j = 0; j < history.count; j++) { + compareEntries(i, j, history); } } diff --git a/browser/components/sessionstore/test/browser_705597.js b/browser/components/sessionstore/test/browser_705597.js index d497e46a97..10f4f08863 100644 --- a/browser/components/sessionstore/test/browser_705597.js +++ b/browser/components/sessionstore/test/browser_705597.js @@ -26,14 +26,8 @@ function test() { let browser = tab.linkedBrowser; promiseTabState(tab, tabState).then(() => { - let entry; - if (!Services.appinfo.sessionHistoryInParent) { - let sessionHistory = browser.sessionHistory; - entry = sessionHistory.legacySHistory.getEntryAtIndex(0); - } else { - let sessionHistory = browser.browsingContext.sessionHistory; - entry = sessionHistory.getEntryAtIndex(0); - } + let sessionHistory = browser.browsingContext.sessionHistory; + let entry = sessionHistory.getEntryAtIndex(0); whenChildCount(entry, 1, function () { whenChildCount(entry, 2, function () { diff --git a/browser/components/sessionstore/test/browser_707862.js b/browser/components/sessionstore/test/browser_707862.js index 765c63257f..4559362e21 100644 --- a/browser/components/sessionstore/test/browser_707862.js +++ b/browser/components/sessionstore/test/browser_707862.js @@ -26,26 +26,14 @@ function test() { let browser = tab.linkedBrowser; promiseTabState(tab, tabState).then(() => { - let entry; - if (!Services.appinfo.sessionHistoryInParent) { - let sessionHistory = browser.sessionHistory; - entry = sessionHistory.legacySHistory.getEntryAtIndex(0); - } else { - let sessionHistory = browser.browsingContext.sessionHistory; - entry = sessionHistory.getEntryAtIndex(0); - } + let sessionHistory = browser.browsingContext.sessionHistory; + let entry = sessionHistory.getEntryAtIndex(0); whenChildCount(entry, 1, function () { whenChildCount(entry, 2, function () { promiseBrowserLoaded(browser).then(() => { - let newEntry; - if (!Services.appinfo.sessionHistoryInParent) { - let newSessionHistory = browser.sessionHistory; - newEntry = newSessionHistory.legacySHistory.getEntryAtIndex(0); - } else { - let newSessionHistory = browser.browsingContext.sessionHistory; - newEntry = newSessionHistory.getEntryAtIndex(0); - } + let newSessionHistory = browser.browsingContext.sessionHistory; + let newEntry = newSessionHistory.getEntryAtIndex(0); whenChildCount(newEntry, 0, function () { // Make sure that we reset the state. diff --git a/browser/components/sessionstore/test/browser_739531.js b/browser/components/sessionstore/test/browser_739531.js index 507d10a5f1..e02a94d9a7 100644 --- a/browser/components/sessionstore/test/browser_739531.js +++ b/browser/components/sessionstore/test/browser_739531.js @@ -19,7 +19,7 @@ function test() { removeFunc = BrowserTestUtils.addContentEventListener( tab.linkedBrowser, "load", - function onLoad(aEvent) { + function onLoad() { // make sure both the page and the frame are loaded if (++loadCount < 2) { return; diff --git a/browser/components/sessionstore/test/browser_async_flushes.js b/browser/components/sessionstore/test/browser_async_flushes.js index e35593dc30..d0bf039ff2 100644 --- a/browser/components/sessionstore/test/browser_async_flushes.js +++ b/browser/components/sessionstore/test/browser_async_flushes.js @@ -44,58 +44,6 @@ add_task(async function test_flush() { gBrowser.removeTab(tab); }); -add_task(async function test_crash() { - if (Services.appinfo.sessionHistoryInParent) { - // This test relies on frame script message ordering. Since the frame script - // is unused with SHIP, there's no guarantee that we'll crash the frame - // before we've started the flush. - ok(true, "Test relies on frame script message ordering."); - return; - } - - // Create new tab. - let tab = BrowserTestUtils.addTab(gBrowser, URL); - gBrowser.selectedTab = tab; - let browser = tab.linkedBrowser; - await promiseBrowserLoaded(browser); - - // Flush to empty any queued update messages. - await TabStateFlusher.flush(browser); - - // There should be one history entry. - let { entries } = JSON.parse(ss.getTabState(tab)); - is(entries.length, 1, "there is a single history entry"); - - // Click the link to navigate. - await SpecialPowers.spawn(browser, [], async function () { - return new Promise(resolve => { - docShell.chromeEventHandler.addEventListener( - "hashchange", - () => resolve(), - { once: true, capture: true } - ); - - // Click the link. - content.document.querySelector("a").click(); - }); - }); - - // Crash the browser and flush. Both messages are async and will be sent to - // the content process. The "crash" message makes it first so that we don't - // get a chance to process the flush. The TabStateFlusher however should be - // notified so that the flush still completes. - let promise1 = BrowserTestUtils.crashFrame(browser); - let promise2 = TabStateFlusher.flush(browser); - await Promise.all([promise1, promise2]); - - // The pending update should be lost. - ({ entries } = JSON.parse(ss.getTabState(tab))); - is(entries.length, 1, "still only one history entry"); - - // Cleanup. - gBrowser.removeTab(tab); -}); - add_task(async function test_remove() { // Create new tab. let tab = BrowserTestUtils.addTab(gBrowser, URL); diff --git a/browser/components/sessionstore/test/browser_async_remove_tab.js b/browser/components/sessionstore/test/browser_async_remove_tab.js index 7f74c57b40..1e3a75adfa 100644 --- a/browser/components/sessionstore/test/browser_async_remove_tab.js +++ b/browser/components/sessionstore/test/browser_async_remove_tab.js @@ -92,15 +92,7 @@ add_task(async function save_worthy_tabs_remote_final() { ok(browser.isRemoteBrowser, "browser is still remote"); // Remove the tab before the update arrives. - let promise = promiseRemoveTabAndSessionState(tab); - - // With SHIP, we'll do the final tab state update sooner than we did before. - if (!Services.appinfo.sessionHistoryInParent) { - // No tab state worth saving (that we know about yet). - ok(!isValueInClosedData(r), "closed tab not saved"); - } - - await promise; + await promiseRemoveTabAndSessionState(tab); // Turns out there is a tab state worth saving. ok(isValueInClosedData(r), "closed tab saved"); @@ -117,15 +109,7 @@ add_task(async function save_worthy_tabs_nonremote_final() { ok(!browser.isRemoteBrowser, "browser is not remote anymore"); // Remove the tab before the update arrives. - let promise = promiseRemoveTabAndSessionState(tab); - - // With SHIP, we'll do the final tab state update sooner than we did before. - if (!Services.appinfo.sessionHistoryInParent) { - // No tab state worth saving (that we know about yet). - ok(!isValueInClosedData(r), "closed tab not saved"); - } - - await promise; + await promiseRemoveTabAndSessionState(tab); // Turns out there is a tab state worth saving. ok(isValueInClosedData(r), "closed tab saved"); @@ -151,15 +135,7 @@ add_task(async function dont_save_empty_tabs_final() { await entryReplaced; // Remove the tab before the update arrives. - let promise = promiseRemoveTabAndSessionState(tab); - - // With SHIP, we'll do the final tab state update sooner than we did before. - if (!Services.appinfo.sessionHistoryInParent) { - // Tab state deemed worth saving (yet). - ok(isValueInClosedData(r), "closed tab saved"); - } - - await promise; + await promiseRemoveTabAndSessionState(tab); // Turns out we don't want to save the tab state. ok(!isValueInClosedData(r), "closed tab not saved"); diff --git a/browser/components/sessionstore/test/browser_async_window_flushing.js b/browser/components/sessionstore/test/browser_async_window_flushing.js index d346f9eb1f..42e24bdd83 100644 --- a/browser/components/sessionstore/test/browser_async_window_flushing.js +++ b/browser/components/sessionstore/test/browser_async_window_flushing.js @@ -116,17 +116,10 @@ add_task(async function test_remove_uninteresting_window() { await SpecialPowers.spawn(browser, [], async function () { // Epic hackery to make this browser seem suddenly boring. docShell.setCurrentURIForSessionStore(Services.io.newURI("about:blank")); - - if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { - let { sessionHistory } = docShell.QueryInterface(Ci.nsIWebNavigation); - sessionHistory.legacySHistory.purgeHistory(sessionHistory.count); - } }); - if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { - let { sessionHistory } = browser.browsingContext; - sessionHistory.purgeHistory(sessionHistory.count); - } + let { sessionHistory } = browser.browsingContext; + sessionHistory.purgeHistory(sessionHistory.count); // Once this windowClosed Promise resolves, we should have finished // the flush and revisited our decision to put this window into diff --git a/browser/components/sessionstore/test/browser_attributes.js b/browser/components/sessionstore/test/browser_attributes.js index a0ee6d5b0c..491ec5db22 100644 --- a/browser/components/sessionstore/test/browser_attributes.js +++ b/browser/components/sessionstore/test/browser_attributes.js @@ -38,45 +38,68 @@ add_task(async function test() { ok(tab.hasAttribute("muted"), "tab.muted exists"); // Make sure we do not persist 'image' and 'muted' attributes. - ss.persistTabAttribute("image"); - ss.persistTabAttribute("muted"); let { attributes } = JSON.parse(ss.getTabState(tab)); ok(!("image" in attributes), "'image' attribute not saved"); ok(!("muted" in attributes), "'muted' attribute not saved"); - ok(!("custom" in attributes), "'custom' attribute not saved"); - - // Test persisting a custom attribute. - tab.setAttribute("custom", "foobar"); - ss.persistTabAttribute("custom"); - - ({ attributes } = JSON.parse(ss.getTabState(tab))); - is(attributes.custom, "foobar", "'custom' attribute is correct"); - - // Make sure we're backwards compatible and restore old 'image' attributes. + ok(!("customizemode" in attributes), "'customizemode' attribute not saved"); + + // Test persisting a customizemode attribute. + { + let customizationReady = BrowserTestUtils.waitForEvent( + gNavToolbox, + "customizationready" + ); + gCustomizeMode.enter(); + await customizationReady; + } + + let customizeIcon = gBrowser.getIcon(gBrowser.selectedTab); + ({ attributes } = JSON.parse(ss.getTabState(gBrowser.selectedTab))); + ok(!("image" in attributes), "'image' attribute not saved"); + is(attributes.customizemode, "true", "'customizemode' attribute is correct"); + + { + let afterCustomization = BrowserTestUtils.waitForEvent( + gNavToolbox, + "aftercustomization" + ); + gCustomizeMode.exit(); + await afterCustomization; + } + + // Test restoring a customizemode tab. let state = { - entries: [{ url: "about:mozilla", triggeringPrincipal_base64 }], - attributes: { custom: "foobaz" }, - image: gBrowser.getIcon(tab), + entries: [], + attributes: { customizemode: "true", nonpersisted: "true" }, }; + // Customize mode doesn't like being restored on top of a non-blank tab. + // For the moment, it appears it isn't possible to restore customizemode onto + // an existing non-blank tab outside of tests, however this may be a latent + // bug if we ever try to do that in the future. + let principal = Services.scriptSecurityManager.createNullPrincipal({}); + tab.linkedBrowser.createAboutBlankDocumentViewer(principal, principal); + // Prepare a pending tab waiting to be restored. let promise = promiseTabRestoring(tab); ss.setTabState(tab, JSON.stringify(state)); await promise; ok(tab.hasAttribute("pending"), "tab is pending"); - is(gBrowser.getIcon(tab), state.image, "tab has correct icon"); + ok(tab.hasAttribute("customizemode"), "tab is in customizemode"); + ok(!tab.hasAttribute("nonpersisted"), "tab has no nonpersisted attribute"); + is(gBrowser.getIcon(tab), customizeIcon, "tab has correct icon"); ok(!state.attributes.image, "'image' attribute not saved"); // Let the pending tab load. gBrowser.selectedTab = tab; - await promiseTabRestored(tab); // Ensure no 'image' or 'pending' attributes are stored. ({ attributes } = JSON.parse(ss.getTabState(tab))); ok(!("image" in attributes), "'image' attribute not saved"); ok(!("pending" in attributes), "'pending' attribute not saved"); - is(attributes.custom, "foobaz", "'custom' attribute is correct"); + ok(!("nonpersisted" in attributes), "'nonpersisted' attribute not saved"); + is(attributes.customizemode, "true", "'customizemode' attribute is correct"); // Clean up. gBrowser.removeTab(tab); diff --git a/browser/components/sessionstore/test/browser_bfcache_telemetry.js b/browser/components/sessionstore/test/browser_bfcache_telemetry.js index 5faa2822ea..c1e9877505 100644 --- a/browser/components/sessionstore/test/browser_bfcache_telemetry.js +++ b/browser/components/sessionstore/test/browser_bfcache_telemetry.js @@ -39,7 +39,6 @@ async function test_bfcache_telemetry(probeInParent) { add_task(async () => { await test_bfcache_telemetry( - Services.appinfo.sessionHistoryInParent && - Services.prefs.getBoolPref("fission.bfcacheInParent") + Services.prefs.getBoolPref("fission.bfcacheInParent") ); }); diff --git a/browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js b/browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js index 081167acfa..c80e63df04 100644 --- a/browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js +++ b/browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js @@ -81,10 +81,6 @@ async function prepareClosedData() { const testWindow7 = await BrowserTestUtils.openNewBrowserWindow(); await openAndCloseTab(testWindow7, TEST_URLS[4]); - let closedTabsHistogram = TelemetryTestUtils.getAndClearHistogram( - "FX_SESSION_RESTORE_CLOSED_TABS_NOT_SAVED" - ); - await BrowserTestUtils.closeWindow(testWindow1); closedIds.testWindow1 = SessionStore.getClosedWindowData()[0].closedId; await BrowserTestUtils.closeWindow(testWindow2); @@ -100,13 +96,7 @@ async function prepareClosedData() { ); await BrowserTestUtils.closeWindow(testWindow6); - TelemetryTestUtils.assertHistogram(closedTabsHistogram, 0, 1); - closedTabsHistogram.clear(); - await BrowserTestUtils.closeWindow(testWindow7); - TelemetryTestUtils.assertHistogram(closedTabsHistogram, 1, 1); - closedTabsHistogram.clear(); - return closedIds; } diff --git a/browser/components/sessionstore/test/browser_cookies.js b/browser/components/sessionstore/test/browser_cookies.js index f514efc777..96244dda1a 100644 --- a/browser/components/sessionstore/test/browser_cookies.js +++ b/browser/components/sessionstore/test/browser_cookies.js @@ -14,7 +14,7 @@ function promiseSetCookie(cookie) { function waitForCookieChanged() { return new Promise(resolve => { - Services.obs.addObserver(function observer(subj, topic, data) { + Services.obs.addObserver(function observer(subj, topic) { Services.obs.removeObserver(observer, topic); resolve(); }, "session-cookie-changed"); diff --git a/browser/components/sessionstore/test/browser_crashedTabs.js b/browser/components/sessionstore/test/browser_crashedTabs.js index 32c064dd81..797cf5ecf8 100644 --- a/browser/components/sessionstore/test/browser_crashedTabs.js +++ b/browser/components/sessionstore/test/browser_crashedTabs.js @@ -82,7 +82,7 @@ function promiseTabCrashedReady(browser) { return new Promise(resolve => { browser.addEventListener( "AboutTabCrashedReady", - function ready(e) { + function ready() { browser.removeEventListener("AboutTabCrashedReady", ready, false, true); resolve(); }, diff --git a/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js b/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js index 1b152139d7..6fc212eb2b 100644 --- a/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js +++ b/browser/components/sessionstore/test/browser_docshell_uuid_consistency.js @@ -4,38 +4,18 @@ add_task(async function duplicateTab() { let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); - if (!Services.appinfo.sessionHistoryInParent) { - await SpecialPowers.spawn(tab.linkedBrowser, [], function () { - let docshell = content.window.docShell.QueryInterface( - Ci.nsIWebNavigation - ); - let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0); - is(shEntry.docshellID.toString(), docshell.historyID.toString()); - }); - } else { - let historyID = tab.linkedBrowser.browsingContext.historyID; - let shEntry = - tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0); - is(shEntry.docshellID.toString(), historyID.toString()); - } + let historyID = tab.linkedBrowser.browsingContext.historyID; + let shEntry = + tab.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0); + is(shEntry.docshellID.toString(), historyID.toString()); let tab2 = gBrowser.duplicateTab(tab); await BrowserTestUtils.browserLoaded(tab2.linkedBrowser); - if (!Services.appinfo.sessionHistoryInParent) { - await SpecialPowers.spawn(tab2.linkedBrowser, [], function () { - let docshell = content.window.docShell.QueryInterface( - Ci.nsIWebNavigation - ); - let shEntry = docshell.sessionHistory.legacySHistory.getEntryAtIndex(0); - is(shEntry.docshellID.toString(), docshell.historyID.toString()); - }); - } else { - let historyID = tab2.linkedBrowser.browsingContext.historyID; - let shEntry = - tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0); - is(shEntry.docshellID.toString(), historyID.toString()); - } + historyID = tab2.linkedBrowser.browsingContext.historyID; + shEntry = + tab2.linkedBrowser.browsingContext.sessionHistory.getEntryAtIndex(0); + is(shEntry.docshellID.toString(), historyID.toString()); BrowserTestUtils.removeTab(tab); BrowserTestUtils.removeTab(tab2); @@ -47,24 +27,10 @@ add_task(async function contentToChromeNavigate() { let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL); await BrowserTestUtils.browserLoaded(tab.linkedBrowser); - if (!Services.appinfo.sessionHistoryInParent) { - await SpecialPowers.spawn(tab.linkedBrowser, [], function () { - let docshell = content.window.docShell.QueryInterface( - Ci.nsIWebNavigation - ); - let sh = docshell.sessionHistory; - is(sh.count, 1); - is( - sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(), - docshell.historyID.toString() - ); - }); - } else { - let historyID = tab.linkedBrowser.browsingContext.historyID; - let sh = tab.linkedBrowser.browsingContext.sessionHistory; - is(sh.count, 1); - is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString()); - } + let historyID = tab.linkedBrowser.browsingContext.historyID; + let sh = tab.linkedBrowser.browsingContext.sessionHistory; + is(sh.count, 1); + is(sh.getEntryAtIndex(0).docshellID.toString(), historyID.toString()); // Force the browser to navigate to the chrome process. BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, "about:config"); @@ -74,31 +40,17 @@ add_task(async function contentToChromeNavigate() { let docShell = tab.linkedBrowser.frameLoader.docShell; // 'cause we're in the chrome process, we can just directly poke at the shistory. - if (!Services.appinfo.sessionHistoryInParent) { - let sh = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; - - is(sh.count, 2); - is( - sh.legacySHistory.getEntryAtIndex(0).docshellID.toString(), - docShell.historyID.toString() - ); - is( - sh.legacySHistory.getEntryAtIndex(1).docshellID.toString(), - docShell.historyID.toString() - ); - } else { - let sh = docShell.browsingContext.sessionHistory; - - is(sh.count, 2); - is( - sh.getEntryAtIndex(0).docshellID.toString(), - docShell.historyID.toString() - ); - is( - sh.getEntryAtIndex(1).docshellID.toString(), - docShell.historyID.toString() - ); - } + sh = docShell.browsingContext.sessionHistory; + + is(sh.count, 2); + is( + sh.getEntryAtIndex(0).docshellID.toString(), + docShell.historyID.toString() + ); + is( + sh.getEntryAtIndex(1).docshellID.toString(), + docShell.historyID.toString() + ); BrowserTestUtils.removeTab(tab); }); diff --git a/browser/components/sessionstore/test/browser_frame_history.js b/browser/components/sessionstore/test/browser_frame_history.js index 1db32e74ab..eeb6de177c 100644 --- a/browser/components/sessionstore/test/browser_frame_history.js +++ b/browser/components/sessionstore/test/browser_frame_history.js @@ -206,7 +206,7 @@ function waitForLoadsInBrowser(aBrowser, aLoadCount) { let loadCount = 0; aBrowser.addEventListener( "load", - function listener(aEvent) { + function listener() { if (++loadCount < aLoadCount) { info( "Got " + loadCount + " loads, waiting until we have " + aLoadCount diff --git a/browser/components/sessionstore/test/browser_frametree.js b/browser/components/sessionstore/test/browser_frametree.js index ce1f5cdf0b..06e0379c59 100644 --- a/browser/components/sessionstore/test/browser_frametree.js +++ b/browser/components/sessionstore/test/browser_frametree.js @@ -98,7 +98,7 @@ add_task(async function test_frametree_dynamic() { is(await enumerateIndexes(browser), "0,1", "correct indexes 0 and 1"); // Remopve a non-dynamic iframe. - await SpecialPowers.spawn(browser, [URL], async ([url]) => { + await SpecialPowers.spawn(browser, [URL], async () => { // Remove the first iframe, which should be a non-dynamic iframe. content.document.body.removeChild( content.document.getElementsByTagName("iframe")[0] diff --git a/browser/components/sessionstore/test/browser_history_persist.js b/browser/components/sessionstore/test/browser_history_persist.js index f6749b02e3..1cf8bf1b8d 100644 --- a/browser/components/sessionstore/test/browser_history_persist.js +++ b/browser/components/sessionstore/test/browser_history_persist.js @@ -25,54 +25,27 @@ add_task(async function check_history_not_persisted() { browser = tab.linkedBrowser; await promiseTabState(tab, state); - if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { - await SpecialPowers.spawn(browser, [], function () { - let sessionHistory = - docShell.browsingContext.childSessionHistory.legacySHistory; - - is(sessionHistory.count, 1, "Should be a single history entry"); - is( - sessionHistory.getEntryAtIndex(0).URI.spec, - "about:blank", - "Should be the right URL" - ); - }); - } else { - let sessionHistory = browser.browsingContext.sessionHistory; - - is(sessionHistory.count, 1, "Should be a single history entry"); - is( - sessionHistory.getEntryAtIndex(0).URI.spec, - "about:blank", - "Should be the right URL" - ); - } + let sessionHistory = browser.browsingContext.sessionHistory; + + is(sessionHistory.count, 1, "Should be a single history entry"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:blank", + "Should be the right URL" + ); // Load a new URL into the tab, it should replace the about:blank history entry BrowserTestUtils.startLoadingURIString(browser, "about:robots"); await promiseBrowserLoaded(browser, false, "about:robots"); - if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { - await SpecialPowers.spawn(browser, [], function () { - let sessionHistory = - docShell.browsingContext.childSessionHistory.legacySHistory; - - is(sessionHistory.count, 1, "Should be a single history entry"); - is( - sessionHistory.getEntryAtIndex(0).URI.spec, - "about:robots", - "Should be the right URL" - ); - }); - } else { - let sessionHistory = browser.browsingContext.sessionHistory; - - is(sessionHistory.count, 1, "Should be a single history entry"); - is( - sessionHistory.getEntryAtIndex(0).URI.spec, - "about:robots", - "Should be the right URL" - ); - } + + sessionHistory = browser.browsingContext.sessionHistory; + + is(sessionHistory.count, 1, "Should be a single history entry"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:robots", + "Should be the right URL" + ); // Cleanup. BrowserTestUtils.removeTab(tab); @@ -99,64 +72,33 @@ add_task(async function check_history_default_persisted() { tab = BrowserTestUtils.addTab(gBrowser, "about:blank"); browser = tab.linkedBrowser; await promiseTabState(tab, state); - if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { - await SpecialPowers.spawn(browser, [], function () { - let sessionHistory = - docShell.browsingContext.childSessionHistory.legacySHistory; - - is(sessionHistory.count, 1, "Should be a single history entry"); - is( - sessionHistory.getEntryAtIndex(0).URI.spec, - "about:blank", - "Should be the right URL" - ); - }); - } else { - let sessionHistory = browser.browsingContext.sessionHistory; - - is(sessionHistory.count, 1, "Should be a single history entry"); - is( - sessionHistory.getEntryAtIndex(0).URI.spec, - "about:blank", - "Should be the right URL" - ); - } + + let sessionHistory = browser.browsingContext.sessionHistory; + + is(sessionHistory.count, 1, "Should be a single history entry"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:blank", + "Should be the right URL" + ); // Load a new URL into the tab, it should replace the about:blank history entry BrowserTestUtils.startLoadingURIString(browser, "about:robots"); await promiseBrowserLoaded(browser, false, "about:robots"); - if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) { - await SpecialPowers.spawn(browser, [], function () { - let sessionHistory = - docShell.browsingContext.childSessionHistory.legacySHistory; - - is(sessionHistory.count, 2, "Should be two history entries"); - is( - sessionHistory.getEntryAtIndex(0).URI.spec, - "about:blank", - "Should be the right URL" - ); - is( - sessionHistory.getEntryAtIndex(1).URI.spec, - "about:robots", - "Should be the right URL" - ); - }); - } else { - let sessionHistory = browser.browsingContext.sessionHistory; - - is(sessionHistory.count, 2, "Should be two history entries"); - is( - sessionHistory.getEntryAtIndex(0).URI.spec, - "about:blank", - "Should be the right URL" - ); - is( - sessionHistory.getEntryAtIndex(1).URI.spec, - "about:robots", - "Should be the right URL" - ); - } + + sessionHistory = browser.browsingContext.sessionHistory; + + is(sessionHistory.count, 2, "Should be two history entries"); + is( + sessionHistory.getEntryAtIndex(0).URI.spec, + "about:blank", + "Should be the right URL" + ); + is( + sessionHistory.getEntryAtIndex(1).URI.spec, + "about:robots", + "Should be the right URL" + ); // Cleanup. BrowserTestUtils.removeTab(tab); diff --git a/browser/components/sessionstore/test/browser_newtab_userTypedValue.js b/browser/components/sessionstore/test/browser_newtab_userTypedValue.js index 755a1f2859..cd17c9a9f0 100644 --- a/browser/components/sessionstore/test/browser_newtab_userTypedValue.js +++ b/browser/components/sessionstore/test/browser_newtab_userTypedValue.js @@ -16,7 +16,7 @@ add_task(async function () { ); // This opens about:newtab: - win.BrowserOpenTab(); + win.BrowserCommands.openTab(); let tab = await tabOpenedAndSwitchedTo; is(win.gURLBar.value, "", "URL bar should be empty"); is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null"); @@ -55,7 +55,7 @@ add_task(async function () { for (let url of gInitialPages) { if (url == BROWSER_NEW_TAB_URL) { - continue; // We tested about:newtab using BrowserOpenTab() above. + continue; // We tested about:newtab using BrowserCommands.openTab() above. } info("Testing " + url + " - " + new Date()); await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url); diff --git a/browser/components/sessionstore/test/browser_oldformat.toml b/browser/components/sessionstore/test/browser_oldformat.toml new file mode 100644 index 0000000000..7edc51dc67 --- /dev/null +++ b/browser/components/sessionstore/test/browser_oldformat.toml @@ -0,0 +1,301 @@ +[DEFAULT] +support-files = [ + "head.js", + "browser_formdata_sample.html", + "browser_formdata_xpath_sample.html", + "browser_frametree_sample.html", + "browser_frametree_sample_frameset.html", + "browser_frametree_sample_iframes.html", + "browser_frame_history_index.html", + "browser_frame_history_index2.html", + "browser_frame_history_index_blank.html", + "browser_frame_history_a.html", + "browser_frame_history_b.html", + "browser_frame_history_c.html", + "browser_frame_history_c1.html", + "browser_frame_history_c2.html", + "browser_formdata_format_sample.html", + "browser_sessionHistory_slow.sjs", + "browser_scrollPositions_sample.html", + "browser_scrollPositions_sample2.html", + "browser_scrollPositions_sample_frameset.html", + "browser_scrollPositions_readerModeArticle.html", + "browser_sessionStorage.html", + "browser_speculative_connect.html", + "browser_248970_b_sample.html", + "browser_339445_sample.html", + "browser_423132_sample.html", + "browser_447951_sample.html", + "browser_454908_sample.html", + "browser_456342_sample.xhtml", + "browser_463205_sample.html", + "browser_463206_sample.html", + "browser_466937_sample.html", + "browser_485482_sample.html", + "browser_637020_slow.sjs", + "browser_662743_sample.html", + "browser_739531_sample.html", + "browser_739531_frame.html", + "browser_911547_sample.html", + "browser_911547_sample.html^headers^", + "coopHeaderCommon.sjs", + "restore_redirect_http.html", + "restore_redirect_http.html^headers^", + "restore_redirect_js.html", + "restore_redirect_target.html", + "browser_1234021_page.html", + "browser_1284886_suspend_tab.html", + "browser_1284886_suspend_tab_2.html", + "empty.html", + "coop_coep.html", + "coop_coep.html^headers^", +] +# remove this after bug 1628486 is landed +prefs = [ + "network.cookie.cookieBehavior=5", + "gfx.font_rendering.fallback.async=false", + "browser.sessionstore.closedTabsFromAllWindows=true", + "browser.sessionstore.closedTabsFromClosedWindows=true", +] + +#NB: the following are disabled +# browser_464620_a.html +# browser_464620_b.html +# browser_464620_xd.html + +#disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html +#disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html +#disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html + +["browser_1234021.js"] + +["browser_1284886_suspend_tab.js"] + +["browser_1446343-windowsize.js"] +skip-if = ["os == 'linux'"] # Bug 1600180 + +["browser_248970_b_perwindowpb.js"] +# Disabled because of leaks. +# Re-enabling and rewriting this test is tracked in bug 936919. +skip-if = ["true"] + +["browser_339445.js"] + +["browser_345898.js"] + +["browser_350525.js"] + +["browser_354894_perwindowpb.js"] + +["browser_367052.js"] + +["browser_393716.js"] +skip-if = ["debug"] # Bug 1507747 + +["browser_394759_basic.js"] +# Disabled for intermittent failures, bug 944372. +skip-if = ["true"] + +["browser_394759_behavior.js"] +https_first_disabled = true + +["browser_394759_perwindowpb.js"] + +["browser_394759_purge.js"] + +["browser_423132.js"] + +["browser_447951.js"] + +["browser_454908.js"] + +["browser_456342.js"] + +["browser_461634.js"] + +["browser_463205.js"] + +["browser_463206.js"] + +["browser_464199.js"] +# Disabled for frequent intermittent failures + +["browser_464620_a.js"] +skip-if = ["true"] + +["browser_464620_b.js"] +skip-if = ["true"] + +["browser_465215.js"] + +["browser_465223.js"] + +["browser_466937.js"] + +["browser_467409-backslashplosion.js"] + +["browser_477657.js"] +skip-if = ["os == 'linux' && os_version == '18.04'"] # bug 1610668 for ubuntu 18.04 + +["browser_480893.js"] + +["browser_485482.js"] + +["browser_485563.js"] + +["browser_490040.js"] + +["browser_491168.js"] + +["browser_491577.js"] +skip-if = [ + "verify && debug && os == 'mac'", + "verify && debug && os == 'win'", +] + +["browser_495495.js"] + +["browser_500328.js"] + +["browser_514751.js"] + +["browser_522375.js"] + +["browser_522545.js"] +skip-if = ["true"] # Bug 1380968 + +["browser_524745.js"] +skip-if = [ + "win10_2009 && !ccov", # Bug 1418627 + "os == 'linux'", # Bug 1803187 +] + +["browser_528776.js"] + +["browser_579868.js"] + +["browser_579879.js"] +skip-if = ["os == 'linux' && (debug || asan)"] # Bug 1234404 + +["browser_581937.js"] + +["browser_586068-apptabs.js"] + +["browser_586068-apptabs_ondemand.js"] +skip-if = ["verify && (os == 'mac' || os == 'win')"] + +["browser_586068-browser_state_interrupted.js"] + +["browser_586068-cascade.js"] + +["browser_586068-multi_window.js"] + +["browser_586068-reload.js"] +https_first_disabled = true + +["browser_586068-select.js"] + +["browser_586068-window_state.js"] + +["browser_586068-window_state_override.js"] + +["browser_586147.js"] + +["browser_588426.js"] + +["browser_590268.js"] + +["browser_590563.js"] + +["browser_595601-restore_hidden.js"] + +["browser_597071.js"] +skip-if = ["true"] # Needs to be rewritten as Marionette test, bug 995916 + +["browser_600545.js"] + +["browser_601955.js"] + +["browser_607016.js"] + +["browser_615394-SSWindowState_events_duplicateTab.js"] + +["browser_615394-SSWindowState_events_setBrowserState.js"] +skip-if = ["verify && debug && os == 'mac'"] + +["browser_615394-SSWindowState_events_setTabState.js"] + +["browser_615394-SSWindowState_events_setWindowState.js"] +https_first_disabled = true + +["browser_615394-SSWindowState_events_undoCloseTab.js"] + +["browser_615394-SSWindowState_events_undoCloseWindow.js"] +skip-if = [ + "os == 'win' && !debug", # Bug 1572554 + "os == 'linux'", # Bug 1572554 +] + +["browser_618151.js"] + +["browser_623779.js"] + +["browser_624727.js"] + +["browser_625016.js"] +skip-if = [ + "os == 'mac'", # Disabled on OS X: + "os == 'linux'", # linux, Bug 1348583 + "os == 'win' && debug", # Bug 1430977 +] + +["browser_628270.js"] + +["browser_635418.js"] + +["browser_636279.js"] + +["browser_637020.js"] + +["browser_645428.js"] + +["browser_659591.js"] + +["browser_662743.js"] + +["browser_662812.js"] +skip-if = ["verify"] + +["browser_665702-state_session.js"] + +["browser_682507.js"] + +["browser_687710.js"] + +["browser_687710_2.js"] +https_first_disabled = true + +["browser_694378.js"] + +["browser_701377.js"] +skip-if = [ + "verify && debug && os == 'win'", + "verify && debug && os == 'mac'", +] + +["browser_705597.js"] + +["browser_707862.js"] + +["browser_739531.js"] + +["browser_739805.js"] + +["browser_819510_perwindowpb.js"] +skip-if = ["true"] # Bug 1284312, Bug 1341980, bug 1381451 + +["browser_906076_lazy_tabs.js"] +https_first_disabled = true +skip-if = ["os == 'linux' && os_version == '18.04'"] # bug 1446464 + +["browser_911547.js"] diff --git a/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js b/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js index 442914d580..ad8144f864 100644 --- a/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js +++ b/browser/components/sessionstore/test/browser_parentProcessRestoreHash.js @@ -12,7 +12,7 @@ const TESTURL = "about:testpageforsessionrestore#foo"; let TestAboutPage = { QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), - getURIFlags(aURI) { + getURIFlags() { // No CAN_ or MUST_LOAD_IN_CHILD means this loads in the parent: return ( Ci.nsIAboutModule.ALLOW_SCRIPT | @@ -73,7 +73,7 @@ add_task(async function () { r => (resolveLocationChangePromise = r) ); let wpl = { - onStateChange(listener, request, state, status) { + onStateChange(listener, request, state, _status) { let location = request.QueryInterface(Ci.nsIChannel).originalURI; // Ignore about:blank loads. let docStop = diff --git a/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js b/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js index cc340c4617..10551238f5 100644 --- a/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js +++ b/browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js @@ -208,7 +208,7 @@ add_task(async function test_reopen_last_tab_if_no_closed_actions() { gBrowser, url: "about:blank", }, - async browser => { + async () => { const TEST_URL = "https://example.com/"; let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); let update = BrowserTestUtils.waitForSessionStoreUpdate(tab); diff --git a/browser/components/sessionstore/test/browser_send_async_message_oom.js b/browser/components/sessionstore/test/browser_send_async_message_oom.js deleted file mode 100644 index 7e807f2fbd..0000000000 --- a/browser/components/sessionstore/test/browser_send_async_message_oom.js +++ /dev/null @@ -1,75 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ -/* eslint-disable mozilla/no-arbitrary-setTimeout */ - -const HISTOGRAM_NAME = "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM"; - -/** - * Test that an OOM in sendAsyncMessage in a framescript will be reported - * to Telemetry. - */ - -add_setup(async function () { - Services.telemetry.canRecordExtended = true; -}); - -function frameScript() { - // Make send[A]syncMessage("SessionStore:update", ...) simulate OOM. - // Other operations are unaffected. - let mm = docShell.messageManager; - - let wrap = function (original) { - return function (name, ...args) { - if (name != "SessionStore:update") { - return original(name, ...args); - } - throw new Components.Exception( - "Simulated OOM", - Cr.NS_ERROR_OUT_OF_MEMORY - ); - }; - }; - - mm.sendAsyncMessage = wrap(mm.sendAsyncMessage.bind(mm)); - mm.sendSyncMessage = wrap(mm.sendSyncMessage.bind(mm)); -} - -add_task(async function () { - // Capture original state. - let snapshot = Services.telemetry.getHistogramById(HISTOGRAM_NAME).snapshot(); - - // Open a browser, configure it to cause OOM. - let newTab = BrowserTestUtils.addTab(gBrowser, "about:robots"); - let browser = newTab.linkedBrowser; - await ContentTask.spawn(browser, null, frameScript); - - let promiseReported = new Promise(resolve => { - browser.messageManager.addMessageListener("SessionStore:error", resolve); - }); - - // Attempt to flush. This should fail. - let promiseFlushed = TabStateFlusher.flush(browser); - promiseFlushed.then(success => { - if (success) { - throw new Error("Flush should have failed"); - } - }); - - // The frame script should report an error. - await promiseReported; - - // Give us some time to handle that error. - await new Promise(resolve => setTimeout(resolve, 10)); - - // By now, Telemetry should have been updated. - let snapshot2 = Services.telemetry - .getHistogramById(HISTOGRAM_NAME) - .snapshot(); - gBrowser.removeTab(newTab); - - Assert.ok(snapshot2.sum > snapshot.sum); -}); - -add_task(async function cleanup() { - Services.telemetry.canRecordExtended = false; -}); diff --git a/browser/components/sessionstore/test/browser_sessionHistory.js b/browser/components/sessionstore/test/browser_sessionHistory.js index 69dcc4995b..34b1ef7d09 100644 --- a/browser/components/sessionstore/test/browser_sessionHistory.js +++ b/browser/components/sessionstore/test/browser_sessionHistory.js @@ -296,12 +296,9 @@ add_task(async function test_slow_subframe_load() { * Ensure that document wireframes can be persisted when they're enabled. */ add_task(async function test_wireframes() { - // Wireframes only works when Fission and SHIP are enabled. - if ( - !Services.appinfo.fissionAutostart || - !Services.appinfo.sessionHistoryInParent - ) { - ok(true, "Skipping test_wireframes when Fission or SHIP is not enabled."); + // Wireframes only works when Fission is enabled. + if (!Services.appinfo.fissionAutostart) { + ok(true, "Skipping test_wireframes when Fission is not enabled."); return; } diff --git a/browser/components/sessionstore/test/browser_sessionStoreContainer.js b/browser/components/sessionstore/test/browser_sessionStoreContainer.js index 86833dea82..e4f3ecea9f 100644 --- a/browser/components/sessionstore/test/browser_sessionStoreContainer.js +++ b/browser/components/sessionstore/test/browser_sessionStoreContainer.js @@ -14,7 +14,7 @@ add_task(async function () { await promiseBrowserLoaded(browser); let tab2 = gBrowser.duplicateTab(tab); - Assert.equal(tab2.getAttribute("usercontextid"), i); + Assert.equal(tab2.getAttribute("usercontextid") || "", i); let browser2 = tab2.linkedBrowser; await promiseTabRestored(tab2); diff --git a/browser/components/sessionstore/test/browser_should_restore_tab.js b/browser/components/sessionstore/test/browser_should_restore_tab.js index ab9513083a..958222141e 100644 --- a/browser/components/sessionstore/test/browser_should_restore_tab.js +++ b/browser/components/sessionstore/test/browser_should_restore_tab.js @@ -13,7 +13,7 @@ async function check_tab_close_notification(openedTab, expectNotification) { let tabClosed = BrowserTestUtils.waitForTabClosing(openedTab); let notified = false; - function topicObserver(_, topic) { + function topicObserver() { notified = true; } Services.obs.addObserver(topicObserver, NOTIFY_CLOSED_OBJECTS_CHANGED); @@ -73,7 +73,7 @@ add_task(async function test_about_new_tab() { () => {} ); // This opens about:newtab: - win.BrowserOpenTab(); + win.BrowserCommands.openTab(); let tab = await tabOpenedAndSwitchedTo; await check_tab_close_notification(tab, false); }); diff --git a/browser/components/sessionstore/test/browser_windowStateContainer.js b/browser/components/sessionstore/test/browser_windowStateContainer.js index f0d6f42d39..e2d2d256eb 100644 --- a/browser/components/sessionstore/test/browser_windowStateContainer.js +++ b/browser/components/sessionstore/test/browser_windowStateContainer.js @@ -11,7 +11,7 @@ add_setup(async function () { function promiseTabsRestored(win, nExpected) { return new Promise(resolve => { let nReceived = 0; - function handler(event) { + function handler() { if (++nReceived === nExpected) { win.gBrowser.tabContainer.removeEventListener( "SSTabRestored", diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js index d475fa86a1..85db6e9d5e 100644 --- a/browser/components/sessionstore/test/head.js +++ b/browser/components/sessionstore/test/head.js @@ -144,7 +144,7 @@ function waitForTopic(aTopic, aTimeout, aCallback) { aCallback(false); }, aTimeout); - function observer(subject, topic, data) { + function observer() { removeObserver(); timeout = clearTimeout(timeout); executeSoon(() => aCallback(true)); @@ -268,7 +268,7 @@ var gWebProgressListener = { } }, - onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, _aStatus) { if ( aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && @@ -298,7 +298,7 @@ var gProgressListener = { } }, - observe(browser, topic, data) { + observe(browser) { gProgressListener.onRestored(browser); }, @@ -451,7 +451,7 @@ function modifySessionStorage(browser, storageData, storageOptions = {}) { return SpecialPowers.spawn( browsingContext, [[storageData, storageOptions]], - async function ([data, options]) { + async function ([data]) { let frame = content; let keys = new Set(Object.keys(data)); let isClearing = !keys.size; @@ -558,35 +558,9 @@ function setPropertyOfFormField(browserContext, selector, propName, newValue) { } function promiseOnHistoryReplaceEntry(browser) { - if (SpecialPowers.Services.appinfo.sessionHistoryInParent) { - return new Promise(resolve => { - let sessionHistory = browser.browsingContext?.sessionHistory; - if (sessionHistory) { - var historyListener = { - OnHistoryNewEntry() {}, - OnHistoryGotoIndex() {}, - OnHistoryPurge() {}, - OnHistoryReload() { - return true; - }, - - OnHistoryReplaceEntry() { - resolve(); - }, - - QueryInterface: ChromeUtils.generateQI([ - "nsISHistoryListener", - "nsISupportsWeakReference", - ]), - }; - - sessionHistory.addSHistoryListener(historyListener); - } - }); - } - - return SpecialPowers.spawn(browser, [], () => { - return new Promise(resolve => { + return new Promise(resolve => { + let sessionHistory = browser.browsingContext?.sessionHistory; + if (sessionHistory) { var historyListener = { OnHistoryNewEntry() {}, OnHistoryGotoIndex() {}, @@ -605,13 +579,8 @@ function promiseOnHistoryReplaceEntry(browser) { ]), }; - var { sessionHistory } = this.docShell.QueryInterface( - Ci.nsIWebNavigation - ); - if (sessionHistory) { - sessionHistory.legacySHistory.addSHistoryListener(historyListener); - } - }); + sessionHistory.addSHistoryListener(historyListener); + } }); } diff --git a/browser/components/shell/HeadlessShell.sys.mjs b/browser/components/shell/HeadlessShell.sys.mjs index c87a7a6d56..7882031613 100644 --- a/browser/components/shell/HeadlessShell.sys.mjs +++ b/browser/components/shell/HeadlessShell.sys.mjs @@ -35,7 +35,7 @@ function loadContentWindow(browser, url) { } const principal = Services.scriptSecurityManager.getSystemPrincipal(); - return new Promise((resolve, reject) => { + return new Promise(resolve => { let oa = E10SUtils.predictOriginAttributes({ browser, }); diff --git a/browser/components/shell/ShellService.sys.mjs b/browser/components/shell/ShellService.sys.mjs index c4af0be7de..ed0c86d1a3 100644 --- a/browser/components/shell/ShellService.sys.mjs +++ b/browser/components/shell/ShellService.sys.mjs @@ -9,6 +9,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", }); XPCOMUtils.defineLazyServiceGetter( @@ -18,6 +19,13 @@ XPCOMUtils.defineLazyServiceGetter( "nsIXREDirProvider" ); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "BackgroundTasks", + "@mozilla.org/backgroundtasks;1", + "nsIBackgroundTasks" +); + ChromeUtils.defineLazyGetter(lazy, "log", () => { let { ConsoleAPI } = ChromeUtils.importESModule( "resource://gre/modules/Console.sys.mjs" @@ -337,6 +345,16 @@ let ShellServiceInternal = { } this.shellService.setDefaultBrowser(forAllUsers); + + // Disable showing toast notification from Firefox Background Tasks. + if (!lazy.BackgroundTasks?.isBackgroundTaskMode) { + await lazy.ASRouter.waitForInitialized; + const win = Services.wm.getMostRecentBrowserWindow() ?? null; + lazy.ASRouter.sendTriggerMessage({ + browser: win, + id: "deeplinkedToWindowsSettingsUI", + }); + } }, async setAsDefault() { diff --git a/browser/components/shell/content/setDesktopBackground.js b/browser/components/shell/content/setDesktopBackground.js index 7448a3e076..70ab825354 100644 --- a/browser/components/shell/content/setDesktopBackground.js +++ b/browser/components/shell/content/setDesktopBackground.js @@ -234,7 +234,7 @@ if (AppConstants.platform != "macosx") { ); }; } else { - gSetBackground.observe = function (aSubject, aTopic, aData) { + gSetBackground.observe = function (aSubject, aTopic) { if (aTopic == "shell:desktop-background-changed") { document.getElementById("setDesktopBackground").hidden = true; document.getElementById("showDesktopPreferences").hidden = false; diff --git a/browser/components/shell/nsIWindowsShellService.idl b/browser/components/shell/nsIWindowsShellService.idl index 13c824f39c..d28b713a78 100644 --- a/browser/components/shell/nsIWindowsShellService.idl +++ b/browser/components/shell/nsIWindowsShellService.idl @@ -112,7 +112,7 @@ interface nsIWindowsShellService : nsISupports * successful or rejects with an nserror. */ [implicit_jscontext] - Promise pinCurrentAppToTaskbarAsync(in bool aPrivateBrowsing); + Promise pinCurrentAppToTaskbarAsync(in boolean aPrivateBrowsing); /* * Do a dry run of pinCurrentAppToTaskbar(). @@ -128,7 +128,7 @@ interface nsIWindowsShellService : nsISupports * @returns same as pinCurrentAppToTaskbarAsync() */ [implicit_jscontext] - Promise checkPinCurrentAppToTaskbarAsync(in bool aPrivateBrowsing); + Promise checkPinCurrentAppToTaskbarAsync(in boolean aPrivateBrowsing); /* * Search for the current executable among taskbar pins @@ -247,7 +247,7 @@ interface nsIWindowsShellService : nsISupports AString classifyShortcut(in AString aPath); [implicit_jscontext] - Promise hasMatchingShortcut(in AString aAUMID, in bool aPrivateBrowsing); + Promise hasMatchingShortcut(in AString aAUMID, in boolean aPrivateBrowsing); /* * Check if setDefaultBrowserUserChoice() is expected to succeed. @@ -257,7 +257,7 @@ interface nsIWindowsShellService : nsISupports * * @return true if the check succeeds, false otherwise. */ - bool canSetDefaultBrowserUserChoice(); + boolean canSetDefaultBrowserUserChoice(); /* * checkAllProgIDsExist() and checkBrowserUserChoiceHashes() are components @@ -265,8 +265,8 @@ interface nsIWindowsShellService : nsISupports * * @return true if the check succeeds, false otherwise. */ - bool checkAllProgIDsExist(); - bool checkBrowserUserChoiceHashes(); + boolean checkAllProgIDsExist(); + boolean checkBrowserUserChoiceHashes(); /* * Determines whether or not Firefox is the "Default Handler", i.e., diff --git a/browser/components/shell/test/browser_1119088.js b/browser/components/shell/test/browser_1119088.js index bc0995fe51..62fc953f44 100644 --- a/browser/components/shell/test/browser_1119088.js +++ b/browser/components/shell/test/browser_1119088.js @@ -101,7 +101,7 @@ add_task(async function () { gBrowser, url: "about:logo", }, - async browser => { + async () => { let dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService( Ci.nsIDirectoryServiceProvider ); diff --git a/browser/components/shell/test/browser_420786.js b/browser/components/shell/test/browser_420786.js index 025cd87943..9cbdc8c4b7 100644 --- a/browser/components/shell/test/browser_420786.js +++ b/browser/components/shell/test/browser_420786.js @@ -14,7 +14,7 @@ add_task(async function () { gBrowser, url: "about:logo", }, - browser => { + () => { var brandName = Services.strings .createBundle("chrome://branding/locale/brand.properties") .GetStringFromName("brandShortName"); diff --git a/browser/components/shell/test/browser_setDesktopBackgroundPreview.js b/browser/components/shell/test/browser_setDesktopBackgroundPreview.js index b2dbe13db8..b8e2a38dd5 100644 --- a/browser/components/shell/test/browser_setDesktopBackgroundPreview.js +++ b/browser/components/shell/test/browser_setDesktopBackgroundPreview.js @@ -12,7 +12,7 @@ add_task(async function () { gBrowser, url: "about:logo", }, - async browser => { + async () => { const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => { await BrowserTestUtils.waitForEvent(win, "load"); Assert.equal( diff --git a/browser/components/shell/test/head.js b/browser/components/shell/test/head.js index db1f8811fd..692ba918d0 100644 --- a/browser/components/shell/test/head.js +++ b/browser/components/shell/test/head.js @@ -89,7 +89,7 @@ async function testWindowSizePositive(width, height) { } let data = await IOUtils.read(screenshotPath); - await new Promise((resolve, reject) => { + await new Promise(resolve => { let blob = new Blob([data], { type: "image/png" }); let reader = new FileReader(); reader.onloadend = function () { @@ -126,7 +126,7 @@ async function testGreen(url, path) { } let data = await IOUtils.read(path); - let image = await new Promise((resolve, reject) => { + let image = await new Promise(resolve => { let blob = new Blob([data], { type: "image/png" }); let reader = new FileReader(); reader.onloadend = function () { diff --git a/browser/components/shopping/tests/browser/browser_exposure_telemetry.js b/browser/components/shopping/tests/browser/browser_exposure_telemetry.js index 51334ce722..f76126aa9d 100644 --- a/browser/components/shopping/tests/browser/browser_exposure_telemetry.js +++ b/browser/components/shopping/tests/browser/browser_exposure_telemetry.js @@ -30,7 +30,7 @@ async function setup(pref) { Services.fog.testResetFOG(); } -async function teardown(pref) { +async function teardown() { await SpecialPowers.popPrefEnv(); await Services.fog.testFlushAllChildren(); Services.fog.testResetFOG(); diff --git a/browser/components/shopping/tests/browser/browser_shopping_settings.js b/browser/components/shopping/tests/browser/browser_shopping_settings.js index 2508be05c7..a32488239e 100644 --- a/browser/components/shopping/tests/browser/browser_shopping_settings.js +++ b/browser/components/shopping/tests/browser/browser_shopping_settings.js @@ -16,7 +16,7 @@ add_task(async function test_shopping_settings_fakespot_learn_more() { await SpecialPowers.spawn( browser, [MOCK_ANALYZED_PRODUCT_RESPONSE], - async mockData => { + async () => { let shoppingContainer = content.document.querySelector( "shopping-container" @@ -55,7 +55,7 @@ add_task(async function test_shopping_settings_ads_learn_more() { await SpecialPowers.spawn( browser, [MOCK_ANALYZED_PRODUCT_RESPONSE], - async mockData => { + async () => { let shoppingContainer = content.document.querySelector( "shopping-container" @@ -404,7 +404,7 @@ add_task( await SpecialPowers.spawn( sidebar.querySelector("browser"), [MOCK_ANALYZED_PRODUCT_RESPONSE], - async mockData => { + async () => { let shoppingContainer = content.document.querySelector( "shopping-container" @@ -490,7 +490,7 @@ add_task( await SpecialPowers.spawn( sidebar.querySelector("browser"), [MOCK_ANALYZED_PRODUCT_RESPONSE], - async mockData => { + async () => { let shoppingContainer = content.document.querySelector( "shopping-container" diff --git a/browser/components/shopping/tests/browser/browser_shopping_urlbar.js b/browser/components/shopping/tests/browser/browser_shopping_urlbar.js index 9eb396e846..d89db3ebcb 100644 --- a/browser/components/shopping/tests/browser/browser_shopping_urlbar.js +++ b/browser/components/shopping/tests/browser/browser_shopping_urlbar.js @@ -7,7 +7,7 @@ const CONTENT_PAGE = "https://example.com"; const PRODUCT_PAGE = "https://example.com/product/B09TJGHL5F"; add_task(async function test_button_hidden() { - await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) { + await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function () { let shoppingButton = document.getElementById("shopping-sidebar-button"); ok( BrowserTestUtils.isHidden(shoppingButton), @@ -17,7 +17,7 @@ add_task(async function test_button_hidden() { }); add_task(async function test_button_shown() { - await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) { + await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function () { let shoppingButton = document.getElementById("shopping-sidebar-button"); ok( BrowserTestUtils.isVisible(shoppingButton), @@ -52,7 +52,7 @@ add_task(async function test_button_changes_with_location() { add_task(async function test_button_active() { Services.prefs.setBoolPref("browser.shopping.experience2023.active", true); - await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) { + await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function () { let shoppingButton = document.getElementById("shopping-sidebar-button"); Assert.equal( shoppingButton.getAttribute("shoppingsidebaropen"), @@ -65,7 +65,7 @@ add_task(async function test_button_active() { add_task(async function test_button_inactive() { Services.prefs.setBoolPref("browser.shopping.experience2023.active", false); - await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) { + await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function () { let shoppingButton = document.getElementById("shopping-sidebar-button"); Assert.equal( shoppingButton.getAttribute("shoppingsidebaropen"), @@ -245,7 +245,7 @@ add_task(async function test_button_right_click_doesnt_affect_sidebars() { add_task(async function test_button_deals_with_tabswitches() { Services.prefs.setBoolPref("browser.shopping.experience2023.active", true); - await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function (browser) { + await BrowserTestUtils.withNewTab(CONTENT_PAGE, async function () { let shoppingButton = document.getElementById("shopping-sidebar-button"); ok( diff --git a/browser/components/shopping/tests/browser/browser_ui_telemetry.js b/browser/components/shopping/tests/browser/browser_ui_telemetry.js index b97aca1963..69bdf50bd1 100644 --- a/browser/components/shopping/tests/browser/browser_ui_telemetry.js +++ b/browser/components/shopping/tests/browser/browser_ui_telemetry.js @@ -259,7 +259,7 @@ add_task(async function test_close_telemetry_recorded() { set: [["browser.shopping.experience2023.active", true]], }); - await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function (browser) { + await BrowserTestUtils.withNewTab(PRODUCT_PAGE, async function () { let shoppingButton = document.getElementById("shopping-sidebar-button"); shoppingButton.click(); }); diff --git a/browser/base/content/browser-sidebar.js b/browser/components/sidebar/browser-sidebar.js index 2d730700a6..55664f8cfc 100644 --- a/browser/base/content/browser-sidebar.js +++ b/browser/components/sidebar/browser-sidebar.js @@ -36,7 +36,9 @@ var SidebarUI = { "viewHistorySidebar", makeSidebar({ elementId: "sidebar-switcher-history", - url: "chrome://browser/content/places/historySidebar.xhtml", + url: this.sidebarRevampEnabled + ? "chrome://browser/content/sidebar/sidebar-history.html" + : "chrome://browser/content/places/historySidebar.xhtml", menuId: "menu_historySidebar", triggerButtonId: "appMenuViewHistorySidebar", }), @@ -45,10 +47,20 @@ var SidebarUI = { "viewTabsSidebar", makeSidebar({ elementId: "sidebar-switcher-tabs", - url: "chrome://browser/content/syncedtabs/sidebar.xhtml", + url: this.sidebarRevampEnabled + ? "chrome://browser/content/sidebar/sidebar-syncedtabs.html" + : "chrome://browser/content/syncedtabs/sidebar.xhtml", menuId: "menu_tabsSidebar", }), ], + [ + "viewMegalistSidebar", + makeSidebar({ + elementId: "sidebar-switcher-megalist", + url: "chrome://global/content/megalist/megalist.html", + menuId: "menu_megalistSidebar", + }), + ], ])); }, @@ -98,7 +110,7 @@ var SidebarUI = { return this._inited; }, - init() { + async init() { this._box = document.getElementById("sidebar-box"); this._splitter = document.getElementById("sidebar-splitter"); this._reversePositionButton = document.getElementById( @@ -108,12 +120,18 @@ var SidebarUI = { this._switcherTarget = document.getElementById("sidebar-switcher-target"); this._switcherArrow = document.getElementById("sidebar-switcher-arrow"); - this._switcherTarget.addEventListener("command", () => { - this.toggleSwitcherPanel(); - }); - this._switcherTarget.addEventListener("keydown", event => { - this.handleKeydown(event); - }); + if (this.sidebarRevampEnabled) { + await import("chrome://browser/content/sidebar/sidebar-launcher.mjs"); + document.getElementById("sidebar-launcher").hidden = false; + document.getElementById("sidebar-header").hidden = true; + } else { + this._switcherTarget.addEventListener("command", () => { + this.toggleSwitcherPanel(); + }); + this._switcherTarget.addEventListener("keydown", event => { + this.handleKeydown(event); + }); + } this._inited = true; @@ -122,6 +140,32 @@ var SidebarUI = { this._initDeferred.resolve(); }, + toggleMegalistItem() { + const sideMenuPopupItem = document.getElementById( + "sidebar-switcher-megalist" + ); + sideMenuPopupItem.style.display = Services.prefs.getBoolPref( + "browser.megalist.enabled", + false + ) + ? "" + : "none"; + }, + + setMegalistMenubarVisibility(aEvent) { + const popup = aEvent.target; + if (popup != aEvent.currentTarget) { + return; + } + + // Show the megalist item if enabled + const megalistItem = popup.querySelector("#menu_megalistSidebar"); + megalistItem.hidden = !Services.prefs.getBoolPref( + "browser.megalist.enabled", + false + ); + }, + uninit() { // If this is the last browser window, persist various values that should be // remembered for after a restart / reopening a browser window. @@ -159,7 +203,7 @@ var SidebarUI = { /** * The handler for Services.obs.addObserver. - **/ + */ observe(_subject, topic, _data) { switch (topic) { case "intl:app-locales-changed": { @@ -216,6 +260,7 @@ var SidebarUI = { /** * Handles keydown on the the switcherTarget button + * * @param {Event} event */ handleKeydown(event) { @@ -241,6 +286,7 @@ var SidebarUI = { }, showSwitcherPanel() { + this.toggleMegalistItem(); this._switcherPanel.addEventListener( "popuphiding", () => { @@ -292,19 +338,25 @@ var SidebarUI = { [...browser.children].forEach((node, i) => { node.style.order = i + 1; }); + let sidebarLauncher = document.querySelector("sidebar-launcher"); if (!this._positionStart) { - // DOM ordering is: | sidebar-box | splitter | appcontent | - // Want to display as: | appcontent | splitter | sidebar-box | - // So we just swap box and appcontent ordering + // DOM ordering is: sidebar-launcher | sidebar-box | splitter | appcontent | + // Want to display as: | appcontent | splitter | sidebar-box | sidebar-launcher + // So we just swap box and appcontent ordering and move sidebar-launcher to the end let appcontent = document.getElementById("appcontent"); let boxOrdinal = this._box.style.order; this._box.style.order = appcontent.style.order; + appcontent.style.order = boxOrdinal; + // the launcher should be on the right of the sidebar-box + sidebarLauncher.style.order = parseInt(this._box.style.order) + 1; // Indicate we've switched ordering to the box this._box.setAttribute("positionend", true); + sidebarLauncher.setAttribute("positionend", true); } else { this._box.removeAttribute("positionend"); + sidebarLauncher.removeAttribute("positionend"); } this.hideSwitcherPanel(); @@ -317,8 +369,9 @@ var SidebarUI = { /** * Try and adopt the status of the sidebar from another window. + * * @param {Window} sourceWindow - Window to use as a source for sidebar status. - * @return true if we adopted the state, or false if the caller should + * @returns {boolean} true if we adopted the state, or false if the caller should * initialize the state itself. */ adoptFromWindow(sourceWindow) { @@ -461,7 +514,7 @@ var SidebarUI = { * @param {string} commandID ID of the sidebar. * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the * visibility toggling of the sidebar. - * @return {Promise} + * @returns {Promise} */ toggle(commandID = this.lastOpenedId, triggerNode) { if ( @@ -508,7 +561,7 @@ var SidebarUI = { * @param {string} commandID ID of the sidebar to use. * @param {DOMNode} [triggerNode] Node, usually a button, that triggered the * showing of the sidebar. - * @return {Promise<boolean>} + * @returns {Promise<boolean>} */ async show(commandID, triggerNode) { let panelType = commandID.substring(4, commandID.length - 7); @@ -537,7 +590,7 @@ var SidebarUI = { * when a window opens (not triggered by user interaction). * * @param {string} commandID ID of the sidebar. - * @return {Promise<boolean>} + * @returns {Promise<boolean>} */ async showInitially(commandID) { let panelType = commandID.substring(4, commandID.length - 7); @@ -559,31 +612,40 @@ var SidebarUI = { * when a window is opened and we don't want to ping telemetry. * * @param {string} commandID ID of the sidebar. - * @return {Promise<void>} + * @returns {Promise<void>} */ _show(commandID) { return new Promise(resolve => { - this.selectMenuItem(commandID); + if (this.sidebarRevampEnabled) { + this._box.dispatchEvent( + new CustomEvent("sidebar-show", { detail: { viewId: commandID } }) + ); + } else { + this.hideSwitcherPanel(); + } + this.selectMenuItem(commandID); this._box.hidden = this._splitter.hidden = false; + // sets the sidebar to the left or right, based on a pref this.setPosition(); - this.hideSwitcherPanel(); - this._box.setAttribute("checked", "true"); this._box.setAttribute("sidebarcommand", commandID); - this.lastOpenedId = commandID; let { url, title, sourceL10nEl } = this.sidebars.get(commandID); + + // use to live update <tree> elements if the locale changes + this.lastOpenedId = commandID; this.title = title; - // Keep the title element in sync with any l10n changes. + // Keep the title element in the switcher in sync with any l10n changes. this.observeTitleChanges(sourceL10nEl); + this.browser.setAttribute("src", url); // kick off async load if (this.browser.contentDocument.location.href != url) { this.browser.addEventListener( "load", - event => { + () => { // We're handling the 'load' event before it bubbles up to the usual // (non-capturing) event handlers. Let it bubble up before resolving. setTimeout(() => { @@ -616,7 +678,9 @@ var SidebarUI = { } this.hideSwitcherPanel(); - + if (this.sidebarRevampEnabled) { + this._box.dispatchEvent(new CustomEvent("sidebar-hide")); + } this.selectMenuItem(""); // Replace the document currently displayed in the sidebar with about:blank @@ -672,3 +736,9 @@ XPCOMUtils.defineLazyPreferenceGetter( true, SidebarUI.setPosition.bind(SidebarUI) ); +XPCOMUtils.defineLazyPreferenceGetter( + SidebarUI, + "sidebarRevampEnabled", + "sidebar.revamp", + false +); diff --git a/browser/components/sidebar/jar.mn b/browser/components/sidebar/jar.mn index c3d7f0cbcf..8a7071ca72 100644 --- a/browser/components/sidebar/jar.mn +++ b/browser/components/sidebar/jar.mn @@ -3,3 +3,12 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. browser.jar: + content/browser/sidebar/browser-sidebar.js + content/browser/sidebar/sidebar-launcher.css + content/browser/sidebar/sidebar-launcher.mjs + content/browser/sidebar/sidebar-history.html + content/browser/sidebar/sidebar-history.mjs + content/browser/sidebar/sidebar-page.mjs + content/browser/sidebar/sidebar-syncedtabs.html + content/browser/sidebar/sidebar-syncedtabs.mjs + content/browser/sidebar/sidebar.css diff --git a/browser/components/sidebar/sidebar-history.html b/browser/components/sidebar/sidebar-history.html new file mode 100644 index 0000000000..f1df5c507a --- /dev/null +++ b/browser/components/sidebar/sidebar-history.html @@ -0,0 +1,34 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta + http-equiv="Content-Security-Policy" + content="default-src resource: chrome:; object-src 'none'; img-src data: chrome:;" + /> + <meta name="color-scheme" content="light dark" /> + <title data-l10n-id="firefoxview-page-title"></title> + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="toolkit/branding/accounts.ftl" /> + <link rel="localization" href="browser/firefoxView.ftl" /> + <link rel="localization" href="preview/sidebar.ftl" /> + <link rel="localization" href="toolkit/branding/brandings.ftl" /> + <link + rel="stylesheet" + href="chrome://browser/content/sidebar/sidebar.css" + /> + <script + type="module" + src="chrome://browser/content/sidebar/sidebar-history.mjs" + ></script> + <script src="chrome://browser/content/contentTheme.js"></script> + </head> + + <body> + <sidebar-history /> + </body> +</html> diff --git a/browser/components/sidebar/sidebar-history.mjs b/browser/components/sidebar/sidebar-history.mjs new file mode 100644 index 0000000000..6c662b2c6f --- /dev/null +++ b/browser/components/sidebar/sidebar-history.mjs @@ -0,0 +1,201 @@ +/* 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 { html, when } from "chrome://global/content/vendor/lit.all.mjs"; + +import { SidebarPage } from "./sidebar-page.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/firefoxview/fxview-search-textbox.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/firefoxview/fxview-tab-list.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-card.mjs"; +import { HistoryController } from "chrome://browser/content/firefoxview/HistoryController.mjs"; +import { navigateToLink } from "chrome://browser/content/firefoxview/helpers.mjs"; + +const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart"; + +export class SidebarHistory extends SidebarPage { + constructor() { + super(); + this._started = false; + // Setting maxTabsLength to -1 for no max + this.maxTabsLength = -1; + } + + controller = new HistoryController(this, { + component: "sidebar", + }); + + connectedCallback() { + super.connectedCallback(); + this.controller.updateAllHistoryItems(); + } + + onPrimaryAction(e) { + navigateToLink(e); + } + + deleteFromHistory() { + this.controller.deleteFromHistory(); + } + + /** + * The template to use for cards-container. + */ + get cardsTemplate() { + if (this.controller.searchResults) { + return this.#searchResultsTemplate(); + } else if (this.controller.allHistoryItems.size) { + return this.#historyCardsTemplate(); + } + return this.#emptyMessageTemplate(); + } + + #historyCardsTemplate() { + let cardsTemplate = []; + this.controller.historyMapByDate.forEach(historyItem => { + if (historyItem.items.length) { + let dateArg = JSON.stringify({ date: historyItem.items[0].time }); + cardsTemplate.push(html`<moz-card + type="accordion" + data-l10n-attrs="heading" + data-l10n-id=${historyItem.l10nId} + data-l10n-args=${dateArg} + > + <div> + <fxview-tab-list + compactRows + class="with-context-menu" + maxTabsLength=${this.maxTabsLength} + .tabItems=${this.getTabItems(historyItem.items)} + @fxview-tab-list-primary-action=${this.onPrimaryAction} + .updatesPaused=${false} + > + </fxview-tab-list> + </div> + </moz-card>`); + } + }); + return cardsTemplate; + } + + #emptyMessageTemplate() { + let descriptionHeader; + let descriptionLabels; + let descriptionLink; + if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) { + // History pref set to never remember history + descriptionHeader = "firefoxview-dont-remember-history-empty-header"; + descriptionLabels = [ + "firefoxview-dont-remember-history-empty-description", + "firefoxview-dont-remember-history-empty-description-two", + ]; + descriptionLink = { + url: "about:preferences#privacy", + name: "history-settings-url-two", + }; + } else { + descriptionHeader = "firefoxview-history-empty-header"; + descriptionLabels = [ + "firefoxview-history-empty-description", + "firefoxview-history-empty-description-two", + ]; + descriptionLink = { + url: "about:preferences#privacy", + name: "history-settings-url", + }; + } + return html` + <fxview-empty-state + headerLabel=${descriptionHeader} + .descriptionLabels=${descriptionLabels} + .descriptionLink=${descriptionLink} + class="empty-state history" + ?isSelectedTab=${this.selectedTab} + mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg" + > + </fxview-empty-state> + `; + } + + #searchResultsTemplate() { + return html` <moz-card + data-l10n-attrs="heading" + data-l10n-id="sidebar-search-results-header" + data-l10n-args=${JSON.stringify({ + query: this.controller.searchQuery, + })} + > + <div> + ${when( + this.controller.searchResults.length, + () => + html`<h3 + slot="secondary-header" + data-l10n-id="firefoxview-search-results-count" + data-l10n-args="${JSON.stringify({ + count: this.controller.searchResults.length, + })}" + ></h3>` + )} + <fxview-tab-list + compactRows + maxTabsLength="-1" + .searchQuery=${this.controller.searchQuery} + .tabItems=${this.getTabItems(this.controller.searchResults)} + @fxview-tab-list-primary-action=${this.onPrimaryAction} + .updatesPaused=${false} + > + </fxview-tab-list> + </div> + </moz-card>`; + } + + async onChangeSortOption(e) { + await this.controller.onChangeSortOption(e); + } + + async onSearchQuery(e) { + await this.controller.onSearchQuery(e); + } + + getTabItems(items) { + return items.map(item => ({ + ...item, + secondaryL10nId: null, + secondaryL10nArgs: null, + })); + } + + render() { + return html` + ${this.stylesheet()} + <div class="container"> + <div class="history-sort-option"> + <div class="history-sort-option"> + <fxview-search-textbox + data-l10n-id="firefoxview-search-text-box-history" + data-l10n-attrs="placeholder" + @fxview-search-textbox-query=${this.onSearchQuery} + .size=${15} + ></fxview-search-textbox> + </div> + </div> + ${this.cardsTemplate} + </div> + `; + } + + willUpdate() { + if (this.controller.allHistoryItems.size) { + // onChangeSortOption() will update history data once it has been fetched + // from the API. + this.controller.createHistoryMaps(); + } + } +} + +customElements.define("sidebar-history", SidebarHistory); diff --git a/browser/components/sidebar/sidebar-launcher.css b/browser/components/sidebar/sidebar-launcher.css new file mode 100644 index 0000000000..b033a650b3 --- /dev/null +++ b/browser/components/sidebar/sidebar-launcher.css @@ -0,0 +1,34 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +.wrapper { + display: grid; + grid-template-rows: auto 1fr; + box-sizing: border-box; + height: 100%; + padding: var(--space-medium); + border-inline-end: 1px solid var(--chrome-content-separator-color); + background-color: var(--sidebar-background-color); + color: var(--sidebar-text-color); + :host([positionend]) & { + border-inline-start: 1px solid var(--chrome-content-separator-color); + border-inline-end: none;; + } +} + +:host([positionend]) { + .wrapper { + border-inline-start: 1px solid var(--chrome-content-separator-color); + } +} + +.actions-list { + display: flex; + flex-direction: column; + justify-content: end; +} + +.icon-button::part(button) { + background-image: var(--action-icon); +} diff --git a/browser/components/sidebar/sidebar-launcher.mjs b/browser/components/sidebar/sidebar-launcher.mjs new file mode 100644 index 0000000000..85eb94b6ca --- /dev/null +++ b/browser/components/sidebar/sidebar-launcher.mjs @@ -0,0 +1,169 @@ +/* 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 { + html, + ifDefined, + styleMap, +} 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-button.mjs"; + +/** + * Vertical strip attached to the launcher that provides an entry point + * to various sidebar panels. + * + */ +export default class SidebarLauncher extends MozLitElement { + static properties = { + topActions: { type: Array }, + bottomActions: { type: Array }, + selectedView: { type: String }, + open: { type: Boolean }, + }; + + constructor() { + super(); + this.topActions = [ + { + icon: `url("chrome://browser/skin/insights.svg")`, + view: null, + l10nId: "sidebar-launcher-insights", + }, + ]; + + this.bottomActions = [ + { + l10nId: "sidebar-menu-history", + icon: `url("chrome://browser/content/firefoxview/view-history.svg")`, + view: "viewHistorySidebar", + }, + { + l10nId: "sidebar-menu-bookmarks", + icon: `url("chrome://browser/skin/bookmark-hollow.svg")`, + view: "viewBookmarksSidebar", + }, + { + l10nId: "sidebar-menu-synced-tabs", + icon: `url("chrome://browser/skin/device-phone.svg")`, + view: "viewTabsSidebar", + }, + ]; + + this.selectedView = window.SidebarUI.currentID; + this.open = window.SidebarUI.isOpen; + this.menuMutationObserver = new MutationObserver(() => + this.#setExtensionItems() + ); + } + + connectedCallback() { + super.connectedCallback(); + this._sidebarBox = document.getElementById("sidebar-box"); + this._sidebarBox.addEventListener("sidebar-show", this); + this._sidebarBox.addEventListener("sidebar-hide", this); + this._sidebarMenu = document.getElementById("viewSidebarMenu"); + + this.menuMutationObserver.observe(this._sidebarMenu, { + childList: true, + subtree: true, + }); + this.#setExtensionItems(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._sidebarBox.removeEventListener("sidebar-show", this); + this._sidebarBox.removeEventListener("sidebar-hide", this); + this.menuMutationObserver.disconnect(); + } + + getImageUrl(icon, targetURI) { + if (window.IS_STORYBOOK) { + return `chrome://global/skin/icons/defaultFavicon.svg`; + } + if (!icon) { + if (targetURI?.startsWith("moz-extension")) { + return "chrome://mozapps/skin/extensions/extension.svg"; + } + return `chrome://global/skin/icons/defaultFavicon.svg`; + } + // If the icon is not for website (doesn't begin with http), we + // display it directly. Otherwise we go through the page-icon + // protocol to try to get a cached version. We don't load + // favicons directly. + if (icon.startsWith("http")) { + return `page-icon:${targetURI}`; + } + return icon; + } + + #setExtensionItems() { + for (let item of this._sidebarMenu.children) { + if (item.id.endsWith("-sidebar-action")) { + this.topActions.push({ + tooltiptext: item.label, + icon: item.style.getPropertyValue("--webextension-menuitem-image"), + view: item.id.slice("menubar_menu_".length), + }); + } + } + } + + handleEvent(e) { + switch (e.type) { + case "sidebar-show": + this.selectedView = e.detail.viewId; + this.open = true; + break; + case "sidebar-hide": + this.open = false; + break; + } + } + + showView(e) { + let view = e.target.getAttribute("view"); + window.SidebarUI.toggle(view); + } + + buttonType(action) { + return this.open && action.view == this.selectedView + ? "icon" + : "icon ghost"; + } + + entrypointTemplate(action) { + return html`<moz-button + class="icon-button" + type=${this.buttonType(action)} + view=${action.view} + @click=${action.view ? this.showView : null} + title=${ifDefined(action.tooltiptext)} + data-l10n-id=${ifDefined(action.l10nId)} + style=${styleMap({ "--action-icon": action.icon })} + > + </moz-button>`; + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://browser/content/sidebar/sidebar-launcher.css" + /> + <div class="wrapper"> + <div class="top-actions actions-list"> + ${this.topActions.map(action => this.entrypointTemplate(action))} + </div> + <div class="bottom-actions actions-list"> + ${this.bottomActions.map(action => this.entrypointTemplate(action))} + </div> + </div> + `; + } +} +customElements.define("sidebar-launcher", SidebarLauncher); diff --git a/browser/components/sidebar/sidebar-page.mjs b/browser/components/sidebar/sidebar-page.mjs new file mode 100644 index 0000000000..157298a561 --- /dev/null +++ b/browser/components/sidebar/sidebar-page.mjs @@ -0,0 +1,45 @@ +/* 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 { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { html } from "chrome://global/content/vendor/lit.all.mjs"; + +export class SidebarPage extends MozLitElement { + constructor() { + super(); + this.clearDocument = this.clearDocument.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + this.ownerGlobal.addEventListener("beforeunload", this.clearDocument); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.ownerGlobal.removeEventListener("beforeunload", this.clearDocument); + } + + /** + * Clear out the document so the disconnectedCallback() will trigger properly + * and all of the custom elements can cleanup. + */ + clearDocument() { + this.ownerGlobal.document.body.textContent = ""; + } + + /** + * The common stylesheet for all sidebar pages. + * + * @returns {TemplateResult} + */ + stylesheet() { + return html` + <link + rel="stylesheet" + href="chrome://browser/content/sidebar/sidebar.css" + /> + `; + } +} diff --git a/browser/components/sidebar/sidebar-syncedtabs.html b/browser/components/sidebar/sidebar-syncedtabs.html new file mode 100644 index 0000000000..6c4874b9ea --- /dev/null +++ b/browser/components/sidebar/sidebar-syncedtabs.html @@ -0,0 +1,45 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta + http-equiv="Content-Security-Policy" + content="default-src resource: chrome:; object-src 'none'; img-src data: chrome:;" + /> + <meta name="color-scheme" content="light dark" /> + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="browser/firefoxView.ftl" /> + <link rel="localization" href="toolkit/branding/accounts.ftl" /> + <link rel="localization" href="toolkit/branding/brandings.ftl" /> + <link rel="stylesheet" href="chrome://global/skin/global.css" /> + <script + type="module" + src="chrome://global/content/elements/moz-card.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/firefoxview/fxview-empty-state.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/firefoxview/fxview-search-textbox.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/firefoxview/fxview-tab-list.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/sidebar/sidebar-syncedtabs.mjs" + ></script> + <script src="chrome://browser/content/contentTheme.js"></script> + </head> + + <body> + <sidebar-syncedtabs /> + </body> +</html> diff --git a/browser/components/sidebar/sidebar-syncedtabs.mjs b/browser/components/sidebar/sidebar-syncedtabs.mjs new file mode 100644 index 0000000000..4c3bd9dc46 --- /dev/null +++ b/browser/components/sidebar/sidebar-syncedtabs.mjs @@ -0,0 +1,191 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs", +}); + +import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs"; +import { + escapeHtmlEntities, + navigateToLink, +} from "chrome://browser/content/firefoxview/helpers.mjs"; + +import { SidebarPage } from "./sidebar-page.mjs"; + +class SyncedTabsInSidebar extends SidebarPage { + controller = new lazy.SyncedTabsController(this); + + constructor() { + super(); + this.onSearchQuery = this.onSearchQuery.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + this.controller.addSyncObservers(); + this.controller.updateStates(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.controller.removeSyncObservers(); + } + + /** + * The template shown when the list of synced devices is currently + * unavailable. + * + * @param {object} options + * @param {string} options.action + * @param {string} options.buttonLabel + * @param {string[]} options.descriptionArray + * @param {string} options.descriptionLink + * @param {boolean} options.error + * @param {string} options.header + * @param {string} options.headerIconUrl + * @param {string} options.mainImageUrl + * @returns {TemplateResult} + */ + messageCardTemplate({ + action, + buttonLabel, + descriptionArray, + descriptionLink, + error, + header, + headerIconUrl, + mainImageUrl, + }) { + return html` + <fxview-empty-state + headerLabel=${header} + .descriptionLabels=${descriptionArray} + .descriptionLink=${ifDefined(descriptionLink)} + class="empty-state synced-tabs error" + isSelectedTab + mainImageUrl="${ifDefined(mainImageUrl)}" + ?errorGrayscale=${error} + headerIconUrl="${ifDefined(headerIconUrl)}" + id="empty-container" + > + <button + class="primary" + slot="primary-action" + ?hidden=${!buttonLabel} + data-l10n-id="${ifDefined(buttonLabel)}" + data-action="${action}" + @click=${e => this.controller.handleEvent(e)} + aria-details="empty-container" + ></button> + </fxview-empty-state> + `; + } + + /** + * The template shown for a device that has tabs. + * + * @param {string} deviceName + * @param {string} deviceType + * @param {Array} tabItems + * @returns {TemplateResult} + */ + deviceTemplate(deviceName, deviceType, tabItems) { + return html`<moz-card + type="accordion" + .heading=${deviceName} + icon + class=${deviceType} + > + <fxview-tab-list + compactRows + .tabItems=${ifDefined(tabItems)} + .updatesPaused=${false} + .searchQuery=${this.controller.searchQuery} + @fxview-tab-list-primary-action=${navigateToLink} + /> + </moz-card>`; + } + + /** + * The template shown for a device that has no tabs. + * + * @param {string} deviceName + * @param {string} deviceType + * @returns {TemplateResult} + */ + noDeviceTabsTemplate(deviceName, deviceType) { + return html`<moz-card + .heading=${deviceName} + icon + class=${deviceType} + data-l10n-id="firefoxview-syncedtabs-device-notabs" + > + </moz-card>`; + } + + /** + * The template shown for a device that has tabs, but no tabs that match the + * current search query. + * + * @param {string} deviceName + * @param {string} deviceType + * @returns {TemplateResult} + */ + noSearchResultsTemplate(deviceName, deviceType) { + return html`<moz-card + .heading=${deviceName} + icon + class=${deviceType} + data-l10n-id="firefoxview-search-results-empty" + data-l10n-args=${JSON.stringify({ + query: escapeHtmlEntities(this.controller.searchQuery), + })} + > + </moz-card>`; + } + + /** + * The template shown for the list of synced devices. + * + * @returns {TemplateResult[]} + */ + deviceListTemplate() { + return Object.values(this.controller.getRenderInfo()).map( + ({ name: deviceName, deviceType, tabItems, tabs }) => { + if (tabItems.length) { + return this.deviceTemplate(deviceName, deviceType, tabItems); + } else if (tabs.length) { + return this.noSearchResultsTemplate(deviceName, deviceType); + } + return this.noDeviceTabsTemplate(deviceName, deviceType); + } + ); + } + + render() { + const messageCard = this.controller.getMessageCard(); + if (messageCard) { + return [this.stylesheet(), this.messageCardTemplate(messageCard)]; + } + return html` + ${this.stylesheet()} + <fxview-search-textbox + data-l10n-id="firefoxview-search-text-box-syncedtabs" + data-l10n-attrs="placeholder" + @fxview-search-textbox-query=${this.onSearchQuery} + size="15" + ></fxview-search-textbox> + ${this.deviceListTemplate()} + `; + } + + onSearchQuery(e) { + this.controller.searchQuery = e.detail.query; + this.requestUpdate(); + } +} + +customElements.define("sidebar-syncedtabs", SyncedTabsInSidebar); diff --git a/browser/components/sidebar/sidebar.css b/browser/components/sidebar/sidebar.css new file mode 100644 index 0000000000..34d43aa850 --- /dev/null +++ b/browser/components/sidebar/sidebar.css @@ -0,0 +1,27 @@ +/* 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 url("chrome://global/skin/global.css"); + +:root { + background-color: var(--lwt-sidebar-background-color); + color: var(--lwt-sidebar-text-color); +} + +moz-card { + margin-block-start: var(--space-medium); + + &.phone::part(icon), + &.mobile::part(icon) { + background-image: url('chrome://browser/skin/device-phone.svg'); + } + + &.desktop::part(icon) { + background-image: url('chrome://browser/skin/device-desktop.svg'); + } + + &.tablet::part(icon) { + background-image: url('chrome://browser/skin/device-tablet.svg'); + } +} diff --git a/browser/components/sidebar/sidebar.ftl b/browser/components/sidebar/sidebar.ftl new file mode 100644 index 0000000000..2a5ef75d83 --- /dev/null +++ b/browser/components/sidebar/sidebar.ftl @@ -0,0 +1,26 @@ +# 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/. + +sidebar-launcher-insights = + .title = Insights + +## Variables: +## $date (string) - Date to be formatted based on locale + +sidebar-history-date-today = + .heading = Today — { DATETIME($date, dateStyle: "full") } +sidebar-history-date-yesterday = + .heading = Yesterday — { DATETIME($date, dateStyle: "full") } +sidebar-history-date-this-month = + .heading = { DATETIME($date, dateStyle: "full") } +sidebar-history-date-prev-month = + .heading = { DATETIME($date, month: "long", year: "numeric") } + +## + +# "Search" is a noun (as in "Results of the search for") +# Variables: +# $query (String) - The search query used for searching through browser history. +sidebar-search-results-header = + .heading = Search results for “{ $query }” diff --git a/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs b/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs index 29b57812bd..51f1f49fa5 100644 --- a/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs +++ b/browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs @@ -10,7 +10,7 @@ import { PseudoLocalizationButton } from "../PseudoLocalizationButton.jsx"; import { FluentPanel } from "../FluentPanel.jsx"; // Register the addon. -addons.register(ADDON_ID, api => { +addons.register(ADDON_ID, () => { // Register the tool. addons.add(TOOL_ID, { type: types.TOOL, diff --git a/browser/components/storybook/.storybook/main.js b/browser/components/storybook/.storybook/main.js index 5791d1e492..0a5a55b851 100644 --- a/browser/components/storybook/.storybook/main.js +++ b/browser/components/storybook/.storybook/main.js @@ -13,17 +13,24 @@ const projectRoot = path.resolve(__dirname, "../../../../"); module.exports = { // The ordering for this stories array affects the order that they are displayed in Storybook stories: [ + // Show the Storybook document first in the list + // so that navigating to firefoxux.github.io/firefox-desktop-components/ + // lands on the Storybook.stories.md file + "../**/README.storybook.stories.md", // Docs section "../**/README.*.stories.md", // UI Widgets section `${projectRoot}/toolkit/content/widgets/**/*.stories.@(js|jsx|mjs|ts|tsx|md)`, // about:logins components stories `${projectRoot}/browser/components/aboutlogins/content/components/**/*.stories.mjs`, + // Reader View components stories + `${projectRoot}/toolkit/components/reader/**/*.stories.mjs`, // Everything else "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx|md)", // Design system files `${projectRoot}/toolkit/themes/shared/design-system/**/*.stories.@(js|jsx|mjs|ts|tsx|md)`, ], + staticDirs: [`${projectRoot}/toolkit/themes/shared/design-system/docs/`], addons: [ "@storybook/addon-links", { @@ -50,7 +57,7 @@ module.exports = { }; return [...existingIndexers, customIndexer]; }, - webpackFinal: async (config, { configType }) => { + webpackFinal: async config => { // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' // You can change the configuration based on that. // 'PRODUCTION' is used when building the static version of storybook. diff --git a/browser/components/storybook/.storybook/manager-head.html b/browser/components/storybook/.storybook/manager-head.html new file mode 100644 index 0000000000..380828d40b --- /dev/null +++ b/browser/components/storybook/.storybook/manager-head.html @@ -0,0 +1,22 @@ +<!-- 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/. --> + +<link + rel="stylesheet" + href="chrome://global/skin/design-system/tokens-brand.css" +/> + +<style> + /* light-dark doesn't work here, using media queries */ + @media (prefers-color-scheme: light) { + iframe { + background-color: var(--color-white) !important; + } + } + @media (prefers-color-scheme: dark) { + iframe { + background-color: var(--color-gray-90) !important; + } + } +</style> diff --git a/browser/components/storybook/.storybook/markdown-story-utils.js b/browser/components/storybook/.storybook/markdown-story-utils.js index 1cc78164ad..5926c5593c 100644 --- a/browser/components/storybook/.storybook/markdown-story-utils.js +++ b/browser/components/storybook/.storybook/markdown-story-utils.js @@ -121,19 +121,21 @@ function getStoryTitle(resourcePath) { * @returns Path used to import a component into a story. */ function getImportPath(resourcePath) { + // We need to normalize the path for this logic to work cross-platform. + let normalizedPath = resourcePath.split(path.sep).join("/"); // Limiting this to toolkit widgets for now since we don't have any // interactive examples in other docs stories. - if (!resourcePath.includes("toolkit/content/widgets")) { + if (!normalizedPath.includes("toolkit/content/widgets")) { return ""; } - let componentName = getComponentName(resourcePath); + let componentName = getComponentName(normalizedPath); let fileExtension = ""; if (componentName) { - let mjsPath = resourcePath.replace( + let mjsPath = normalizedPath.replace( "README.stories.md", `${componentName}.mjs` ); - let jsPath = resourcePath.replace( + let jsPath = normalizedPath.replace( "README.stories.md", `${componentName}.js` ); diff --git a/browser/components/storybook/.storybook/preview-head.html b/browser/components/storybook/.storybook/preview-head.html index 206972e714..ae9d8fdf5a 100644 --- a/browser/components/storybook/.storybook/preview-head.html +++ b/browser/components/storybook/.storybook/preview-head.html @@ -37,8 +37,12 @@ } /* Ensure WithCommonStyles can grow to fit the page */ - #root-inner { - height: 100vh; + html, + body, + #root, + #root-inner, + #storybook-root { + height: 100%; } /* Docs stories are being given unnecessary height, possibly because we diff --git a/browser/components/storybook/.storybook/preview.mjs b/browser/components/storybook/.storybook/preview.mjs index 4e0f3f407d..ec7fd42151 100644 --- a/browser/components/storybook/.storybook/preview.mjs +++ b/browser/components/storybook/.storybook/preview.mjs @@ -54,7 +54,7 @@ class WithCommonStyles extends MozLitElement { font: message-box; font-size: var(--font-size-root); appearance: none; - background-color: var(--color-canvas); + background-color: var(--background-color-canvas); color: var(--text-color); -moz-box-layout: flex; } @@ -113,6 +113,7 @@ export default { title: "On this page", }, }, + options: { showPanel: true }, }, }; diff --git a/browser/components/storybook/docs/README.storybook.stories.md b/browser/components/storybook/docs/README.storybook.stories.md index bb0fcdd1a2..5e94be7761 100644 --- a/browser/components/storybook/docs/README.storybook.stories.md +++ b/browser/components/storybook/docs/README.storybook.stories.md @@ -4,7 +4,7 @@ playground for UI components. We use Storybook to document our design system, reusable components, and any specific components you might want to test with dummy data. [Take a look at our Storybook -instance!](https://firefoxux.github.io/firefox-desktop-components/?path=/story/docs-reusable-widgets--page) +instance!](https://firefoxux.github.io/firefox-desktop-components/) ## Background diff --git a/browser/components/storybook/stories/fxview-tab-list.stories.mjs b/browser/components/storybook/stories/fxview-tab-list.stories.mjs index c8e2328c44..b18ad16e3a 100644 --- a/browser/components/storybook/stories/fxview-tab-list.stories.mjs +++ b/browser/components/storybook/stories/fxview-tab-list.stories.mjs @@ -84,7 +84,7 @@ let secondaryAction = e => { e.target.querySelector("panel-list").toggle(e.detail.originalEvent); }; -let primaryAction = e => { +let primaryAction = () => { // Open in new tab }; diff --git a/browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs b/browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs index 47571f789d..5fe9c8c3e5 100644 --- a/browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs +++ b/browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs @@ -96,7 +96,7 @@ SyncedTabsDeckComponent.prototype = { this._deckView.destroy(); }, - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case this._SyncedTabs.TOPIC_TABS_CHANGED: this._syncedTabsListStore.getData(); diff --git a/browser/components/syncedtabs/TabListView.sys.mjs b/browser/components/syncedtabs/TabListView.sys.mjs index 041d7300d9..70c98c4175 100644 --- a/browser/components/syncedtabs/TabListView.sys.mjs +++ b/browser/components/syncedtabs/TabListView.sys.mjs @@ -127,7 +127,7 @@ TabListView.prototype = { }, // Client rows are hidden when the list is filtered - _renderFilteredClient(client, filter) { + _renderFilteredClient(client) { client.tabs.forEach((tab, index) => { let node = this._renderTab(client, tab, index); this.list.appendChild(node); diff --git a/browser/components/tabpreview/jar.mn b/browser/components/tabpreview/jar.mn index 8ff09ebb17..589bc71430 100644 --- a/browser/components/tabpreview/jar.mn +++ b/browser/components/tabpreview/jar.mn @@ -3,5 +3,5 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. browser.jar: - content/browser/tabpreview/tabpreview.mjs (tabpreview.mjs) + content/browser/tabpreview/tab-preview-panel.mjs (tab-preview-panel.mjs) content/browser/tabpreview/tabpreview.css (tabpreview.css) diff --git a/browser/components/tabpreview/tab-preview-panel.mjs b/browser/components/tabpreview/tab-preview-panel.mjs new file mode 100644 index 0000000000..683b2c17ec --- /dev/null +++ b/browser/components/tabpreview/tab-preview-panel.mjs @@ -0,0 +1,174 @@ +/* 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/. */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const POPUP_OPTIONS = { + position: "bottomleft topleft", + x: 0, + y: -2, +}; + +/** + * Detailed preview card that displays when hovering a tab + */ +export default class TabPreviewPanel { + constructor(panel) { + this._panel = panel; + this._win = panel.ownerGlobal; + this._tab = null; + this._thumbnailElement = null; + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_prefDisableAutohide", + "ui.popup.disable_autohide", + false + ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_prefPreviewDelay", + "ui.tooltip.delay_ms" + ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_prefDisplayThumbnail", + "browser.tabs.cardPreview.showThumbnails", + false + ); + this._timer = null; + } + + getPrettyURI(uri) { + try { + const url = new URL(uri); + return `${url.hostname}`.replace(/^w{3}\./, ""); + } catch { + return uri; + } + } + + _needsThumbnailFor(tab) { + return !tab.selected; + } + + _maybeRequestThumbnail() { + if (!this._prefDisplayThumbnail) { + return; + } + if (!this._needsThumbnailFor(this._tab)) { + return; + } + let tab = this._tab; + this._win.tabPreviews.get(tab).then(el => { + if (this._tab == tab && this._needsThumbnailFor(tab)) { + this._thumbnailElement = el; + this._updatePreview(); + } + }); + } + + activate(tab) { + this._tab = tab; + this._thumbnailElement = null; + this._maybeRequestThumbnail(); + if (this._panel.state == "open") { + this._updatePreview(); + } + if (this._timer) { + return; + } + this._timer = this._win.setTimeout(() => { + this._timer = null; + this._panel.openPopup(this._tab, POPUP_OPTIONS); + }, this._prefPreviewDelay); + this._win.addEventListener("TabSelect", this); + this._panel.addEventListener("popupshowing", this); + } + + deactivate(leavingTab = null) { + if (leavingTab) { + if (this._tab != leavingTab) { + return; + } + this._win.requestAnimationFrame(() => { + if (this._tab == leavingTab) { + this.deactivate(); + } + }); + return; + } + this._tab = null; + this._thumbnailElement = null; + this._panel.removeEventListener("popupshowing", this); + this._win.removeEventListener("TabSelect", this); + if (!this._prefDisableAutohide) { + this._panel.hidePopup(); + } + if (this._timer) { + this._win.clearTimeout(this._timer); + this._timer = null; + } + } + + handleEvent(e) { + switch (e.type) { + case "popupshowing": + this._updatePreview(); + break; + case "TabSelect": + if (this._thumbnailElement && !this._needsThumbnailFor(this._tab)) { + this._thumbnailElement.remove(); + this._thumbnailElement = null; + } + break; + } + } + + _updatePreview() { + this._panel.querySelector(".tab-preview-title").textContent = + this._displayTitle; + this._panel.querySelector(".tab-preview-uri").textContent = + this._displayURI; + let thumbnailContainer = this._panel.querySelector( + ".tab-preview-thumbnail-container" + ); + if (thumbnailContainer.firstChild != this._thumbnailElement) { + thumbnailContainer.replaceChildren(); + if (this._thumbnailElement) { + thumbnailContainer.appendChild(this._thumbnailElement); + } + this._panel.dispatchEvent( + new CustomEvent("previewThumbnailUpdated", { + detail: { + thumbnail: this._thumbnailElement, + }, + }) + ); + } + if (this._tab && this._panel.state == "open") { + this._panel.moveToAnchor( + this._tab, + POPUP_OPTIONS.position, + POPUP_OPTIONS.x, + POPUP_OPTIONS.y + ); + } + } + + get _displayTitle() { + if (!this._tab) { + return ""; + } + return this._tab.textLabel.textContent; + } + + get _displayURI() { + if (!this._tab) { + return ""; + } + return this.getPrettyURI(this._tab.linkedBrowser.currentURI.spec); + } +} diff --git a/browser/components/tabpreview/tabpreview.css b/browser/components/tabpreview/tabpreview.css index 776f520c7d..e978266e5d 100644 --- a/browser/components/tabpreview/tabpreview.css +++ b/browser/components/tabpreview/tabpreview.css @@ -2,32 +2,21 @@ * 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/. */ -.tab-preview-container { - --tab-preview-background-color: light-dark(#fff, #42414d); - --tab-preview-text-color: light-dark(#15141a, #fbfbfe); - --tab-preview-border-color: light-dark(#cfcfd8, #8f8f9d); - - @media (prefers-contrast) { - --tab-preview-background-color: Canvas; - --tab-preview-text-color: CanvasText; - } +#tab-preview-panel { + --panel-width: 280px; + --panel-padding: 0; + --panel-background: var(--tab-selected-bgcolor); + --panel-color: var(--tab-selected-textcolor); } -.tab-preview-container { - background-color: var(--tab-preview-background-color); - color: var(--tab-preview-text-color); - border-radius: 9px; - display: inline-block; - width: 280px; - overflow: hidden; - line-height: 1.5; - border: 1px solid var(--tab-preview-border-color); +.tab-preview-text-container { + padding: var(--space-small); } .tab-preview-title { max-height: 3em; overflow: hidden; - font-weight: 600; + font-weight: var(--font-weight-bold); } .tab-preview-uri { @@ -38,22 +27,18 @@ text-overflow: ellipsis; } -.tab-preview-text-container { - padding: 8px; -} - .tab-preview-thumbnail-container { - border-top: 1px solid var(--tab-preview-border-color); -} - -.tab-preview-thumbnail-container img, -.tab-preview-thumbnail-container canvas { - display: block; - width: 100%; -} - -@media (max-width: 640px) { - .tab-preview-thumbnail-container { + border-top: 1px solid var(--panel-border-color); + &:empty { display: none; } + @media (width < 640px) { + display: none; + } + + > img, + > canvas { + display: block; + width: 100%; + } } diff --git a/browser/components/tabpreview/tabpreview.mjs b/browser/components/tabpreview/tabpreview.mjs deleted file mode 100644 index 2409c3fa7a..0000000000 --- a/browser/components/tabpreview/tabpreview.mjs +++ /dev/null @@ -1,237 +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 { html } from "chrome://global/content/vendor/lit.all.mjs"; -import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; - -var { XPCOMUtils } = ChromeUtils.importESModule( - "resource://gre/modules/XPCOMUtils.sys.mjs" -); - -const TAB_PREVIEW_USE_THUMBNAILS_PREF = - "browser.tabs.cardPreview.showThumbnails"; - -/** - * Detailed preview card that displays when hovering a tab - * - * @property {MozTabbrowserTab} tab - the tab to preview - * @fires TabPreview#previewhidden - * @fires TabPreview#previewshown - * @fires TabPreview#previewThumbnailUpdated - */ -export default class TabPreview extends MozLitElement { - static properties = { - tab: { type: Object }, - - _previewIsActive: { type: Boolean, state: true }, - _previewDelayTimeout: { type: Number, state: true }, - _displayTitle: { type: String, state: true }, - _displayURI: { type: String, state: true }, - _displayImg: { type: Object, state: true }, - }; - - constructor() { - super(); - XPCOMUtils.defineLazyPreferenceGetter( - this, - "_prefPreviewDelay", - "ui.tooltip.delay_ms" - ); - XPCOMUtils.defineLazyPreferenceGetter( - this, - "_prefDisplayThumbnail", - TAB_PREVIEW_USE_THUMBNAILS_PREF, - false - ); - } - - // render this inside a <panel> - createRenderRoot() { - if (!document.createXULElement) { - console.error( - "Unable to create panel: document.createXULElement is not available" - ); - return super.createRenderRoot(); - } - this.attachShadow({ mode: "open" }); - this.panel = document.createXULElement("panel"); - this.panel.setAttribute("id", "tabPreviewPanel"); - this.panel.setAttribute("noautofocus", true); - this.panel.setAttribute("norolluponanchor", true); - this.panel.setAttribute("consumeoutsideclicks", "never"); - this.panel.setAttribute("rolluponmousewheel", "true"); - this.panel.setAttribute("level", "parent"); - this.shadowRoot.append(this.panel); - return this.panel; - } - - get previewCanShow() { - return this._previewIsActive && this.tab; - } - - get thumbnailCanShow() { - return ( - this.previewCanShow && - this._prefDisplayThumbnail && - !this.tab.selected && - this._displayImg - ); - } - - getPrettyURI(uri) { - try { - const url = new URL(uri); - return `${url.hostname}`.replace(/^w{3}\./, ""); - } catch { - return uri; - } - } - - handleEvent(e) { - switch (e.type) { - case "TabSelect": { - this.requestUpdate(); - break; - } - case "popuphidden": { - this.previewHidden(); - break; - } - } - } - - showPreview() { - this.panel.openPopup(this.tab, { - position: "bottomleft topleft", - y: -2, - isContextMenu: false, - }); - window.addEventListener("TabSelect", this); - this.panel.addEventListener("popuphidden", this); - } - - hidePreview() { - this.panel.hidePopup(); - } - - previewHidden() { - window.removeEventListener("TabSelect", this); - this.panel.removeEventListener("popuphidden", this); - - /** - * @event TabPreview#previewhidden - * @type {CustomEvent} - */ - this.dispatchEvent(new CustomEvent("previewhidden")); - } - - // compute values derived from tab element - willUpdate(changedProperties) { - if (!changedProperties.has("tab")) { - return; - } - if (!this.tab) { - this._displayTitle = ""; - this._displayURI = ""; - this._displayImg = null; - return; - } - this._displayTitle = this.tab.textLabel.textContent; - this._displayURI = this.getPrettyURI( - this.tab.linkedBrowser.currentURI.spec - ); - this._displayImg = null; - let { tab } = this; - window.tabPreviews.get(this.tab).then(el => { - if (this.tab == tab) { - this._displayImg = el; - } - }); - } - - updated(changedProperties) { - if (changedProperties.has("tab")) { - // handle preview delay - if (!this.tab) { - clearTimeout(this._previewDelayTimeout); - this._previewIsActive = false; - } else { - let lastTabVal = changedProperties.get("tab"); - if (!lastTabVal) { - // tab was set from an empty state, - // so wait for the delay duration before showing - this._previewDelayTimeout = setTimeout(() => { - this._previewIsActive = true; - }, this._prefPreviewDelay); - } - } - } - if (changedProperties.has("_previewIsActive")) { - if (!this._previewIsActive) { - this.hidePreview(); - } - } - if ( - (changedProperties.has("tab") || - changedProperties.has("_previewIsActive")) && - this.previewCanShow - ) { - this.updateComplete.then(() => { - if (this.panel.state == "open" || this.panel.state == "showing") { - this.panel.moveToAnchor(this.tab, "bottomleft topleft", 0, -2); - } else { - this.showPreview(); - } - - this.dispatchEvent( - /** - * @event TabPreview#previewshown - * @type {CustomEvent} - * @property {object} detail - * @property {MozTabbrowserTab} detail.tab - the tab being previewed - */ - new CustomEvent("previewshown", { - detail: { tab: this.tab }, - }) - ); - }); - } - if (changedProperties.has("_displayImg")) { - this.updateComplete.then(() => { - /** - * fires when the thumbnail for a preview is loaded - * and added to the document. - * - * @event TabPreview#previewThumbnailUpdated - * @type {CustomEvent} - */ - this.dispatchEvent(new CustomEvent("previewThumbnailUpdated")); - }); - } - } - - render() { - return html` - <link - rel="stylesheet" - type="text/css" - href="chrome://browser/content/tabpreview/tabpreview.css" - /> - <div class="tab-preview-container"> - <div class="tab-preview-text-container"> - <div class="tab-preview-title">${this._displayTitle}</div> - <div class="tab-preview-uri">${this._displayURI}</div> - </div> - ${this.thumbnailCanShow - ? html` - <div class="tab-preview-thumbnail-container"> - ${this._displayImg} - </div> - ` - : ""} - </div> - `; - } -} -customElements.define("tab-preview", TabPreview); diff --git a/browser/components/tests/browser/browser_contentpermissionprompt.js b/browser/components/tests/browser/browser_contentpermissionprompt.js index 3e2eb24f62..11b18a6653 100644 --- a/browser/components/tests/browser/browser_contentpermissionprompt.js +++ b/browser/components/tests/browser/browser_contentpermissionprompt.js @@ -151,7 +151,7 @@ add_task(async function test_working_request() { }, }; - let integration = base => ({ + let integration = () => ({ createPermissionPrompt(type, request) { Assert.equal(type, "test-permission-type"); Assert.ok( diff --git a/browser/components/tests/browser/browser_default_webprotocol_handler_mailto.js b/browser/components/tests/browser/browser_default_webprotocol_handler_mailto.js index d061d84b23..7bb8b22a98 100644 --- a/browser/components/tests/browser/browser_default_webprotocol_handler_mailto.js +++ b/browser/components/tests/browser/browser_default_webprotocol_handler_mailto.js @@ -11,12 +11,43 @@ add_setup(function add_setup() { Services.prefs.setBoolPref("browser.mailto.dualPrompt", true); }); +/* helper function to delete site specific settings needed to clean up + * the testing setup after some of these tests. + * + * @see: nsIContentPrefService2.idl + */ +function _deleteSiteSpecificSetting(domain, setting, context = null) { + const contentPrefs = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + + return contentPrefs.removeByDomainAndName(domain, setting, context, { + handleResult(_) {}, + handleCompletion() { + Assert.ok(true, "setting successfully deleted."); + }, + handleError(_) { + Assert.ok(false, "could not delete site specific setting."); + }, + }); +} + // site specific settings const protocol = "mailto"; -const sss_domain = "test.example.com"; +const subdomain = (Math.random() + 1).toString(36).substring(7); +const sss_domain = subdomain + ".example.com"; const ss_setting = "system.under.test"; add_task(async function check_null_value() { + Assert.ok( + /[a-z0-9].+\.[a-z]+\.[a-z]+/.test(sss_domain), + "test the validity of this random domain name before using it for tests: '" + + sss_domain + + "'" + ); +}); + +add_task(async function check_null_value() { Assert.equal( null, await WebProtocolHandlerRegistrar._getSiteSpecificSetting( @@ -46,13 +77,31 @@ add_task(async function check_save_value() { ss_setting, ss_setting ); + + let fetchedSiteSpecificSetting; + try { + fetchedSiteSpecificSetting = + await WebProtocolHandlerRegistrar._getSiteSpecificSetting( + sss_domain, + ss_setting + ); + } finally { + // make sure the cleanup happens, no matter what + _deleteSiteSpecificSetting(sss_domain, ss_setting); + } Assert.equal( ss_setting, + fetchedSiteSpecificSetting, + "site specific setting save and retrieve test." + ); + + Assert.equal( + null, await WebProtocolHandlerRegistrar._getSiteSpecificSetting( sss_domain, ss_setting ), - "site specific setting save and retrieve test." + "site specific setting should not exist after delete." ); }); diff --git a/browser/components/tests/browser/browser_quit_disabled.js b/browser/components/tests/browser/browser_quit_disabled.js index 3b7e99a1bf..a2151e8953 100644 --- a/browser/components/tests/browser/browser_quit_disabled.js +++ b/browser/components/tests/browser/browser_quit_disabled.js @@ -27,7 +27,7 @@ add_task(async function test_quit_shortcut_disabled() { let quitRequested = false; let observer = { - observe(subject, topic, data) { + observe(subject, topic) { is(topic, "quit-application-requested", "Right observer topic"); ok(shouldQuit, "Quit shortcut should NOT have worked"); diff --git a/browser/components/tests/browser/head.js b/browser/components/tests/browser/head.js index 89c8df8613..28b14aef0b 100644 --- a/browser/components/tests/browser/head.js +++ b/browser/components/tests/browser/head.js @@ -42,7 +42,7 @@ function mockShell(overrides = {}) { isDefault: false, isPinned: false, - async checkPinCurrentAppToTaskbarAsync(privateBrowsing = false) { + async checkPinCurrentAppToTaskbarAsync() { if (!this.canPin) { throw Error; } @@ -50,7 +50,7 @@ function mockShell(overrides = {}) { get isAppInDock() { return this.isPinned; }, - isCurrentAppPinnedToTaskbarAsync(privateBrowsing = false) { + isCurrentAppPinnedToTaskbarAsync() { return Promise.resolve(this.isPinned); }, isDefaultBrowser() { diff --git a/browser/components/touchbar/MacTouchBar.sys.mjs b/browser/components/touchbar/MacTouchBar.sys.mjs index 5b598efc00..6588920f5e 100644 --- a/browser/components/touchbar/MacTouchBar.sys.mjs +++ b/browser/components/touchbar/MacTouchBar.sys.mjs @@ -107,7 +107,7 @@ var gBuiltInInputs = { type: kInputTypes.BUTTON, callback: () => { let win = lazy.BrowserWindowTracker.getTopWindow(); - win.BrowserHome(); + win.BrowserCommands.home(); }, }, Fullscreen: { diff --git a/browser/components/touchbar/tests/browser/browser_touchbar_tests.js b/browser/components/touchbar/tests/browser/browser_touchbar_tests.js index c6326b4509..0da72143d1 100644 --- a/browser/components/touchbar/tests/browser/browser_touchbar_tests.js +++ b/browser/components/touchbar/tests/browser/browser_touchbar_tests.js @@ -139,7 +139,7 @@ function waitForFullScreenState(browser, state) { return new Promise(resolve => { let eventReceived = false; - let observe = (subject, topic, data) => { + let observe = () => { if (!eventReceived) { return; } diff --git a/browser/components/translations/content/TranslationsPanelShared.sys.mjs b/browser/components/translations/content/TranslationsPanelShared.sys.mjs index 570528df3f..f5045f57e0 100644 --- a/browser/components/translations/content/TranslationsPanelShared.sys.mjs +++ b/browser/components/translations/content/TranslationsPanelShared.sys.mjs @@ -11,9 +11,53 @@ ChromeUtils.defineESModuleGetters(lazy, { /** * A class containing static functionality that is shared by both * the FullPageTranslationsPanel and SelectTranslationsPanel classes. + * + * It is recommended to read the documentation above the TranslationsParent class + * definition to understand the scope of the Translations architecture throughout + * Firefox. + * + * @see TranslationsParent + * + * The static instance of this class is a singleton in the parent process, and is + * available throughout all windows and tabs, just like the static instance of + * the TranslationsParent class. + * + * Unlike the TranslationsParent, this class is never instantiated as an actor + * outside of the static-context functionality defined below. */ export class TranslationsPanelShared { - static #langListsInitState = new Map(); + /** + * A map from Translations Panel instances to their initialized states. + * There is one instance of each panel per top ChromeWindow in Firefox. + * + * See the documentation above the TranslationsParent class for a detailed + * explanation of the translations architecture throughout Firefox. + * + * @see TranslationsParent + * + * @type {Map<FullPageTranslationsPanel | SelectTranslationsPanel, string>} + */ + static #langListsInitState = new WeakMap(); + + /** + * True if the next language-list initialization to fail for testing. + * + * @see TranslationsPanelShared.ensureLangListsBuilt + * + * @type {boolean} + */ + static #simulateLangListError = false; + + /** + * Clears cached data regarding the initialization state of the + * FullPageTranslationsPanel or the SelectTranslationsPanel. + * + * This is only needed for test runners to ensure that each test + * starts from a clean slate. + */ + static clearCache() { + this.#langListsInitState = new WeakMap(); + } /** * Defines lazy getters for accessing elements in the document based on provided entries. @@ -46,13 +90,25 @@ export class TranslationsPanelShared { } /** + * Ensures that the next call to ensureLangListBuilt wil fail + * for the purpose of testing the error state. + * + * @see TranslationsPanelShared.ensureLangListsBuilt + * + * @type {boolean} + */ + static simulateLangListError() { + this.#simulateLangListError = true; + } + + /** * Retrieves the initialization state of language lists for the specified panel. * * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel * - The panel for which to look up the state. */ static getLangListsInitState(panel) { - return TranslationsPanelShared.#langListsInitState.get(panel.id); + return TranslationsPanelShared.#langListsInitState.get(panel); } /** @@ -64,17 +120,17 @@ export class TranslationsPanelShared { * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel * - The panel for which to ensure language lists are built. */ - static async ensureLangListsBuilt(document, panel, innerWindowId) { - const { id } = panel; - switch ( - TranslationsPanelShared.#langListsInitState.get(`${id}-${innerWindowId}`) - ) { + static async ensureLangListsBuilt(document, panel) { + const { panel: panelElement } = panel.elements; + switch (TranslationsPanelShared.#langListsInitState.get(panel)) { case "initialized": // This has already been initialized. return; case "error": case undefined: - // attempt to initialize + // Set the error state in case there is an early exit at any point. + // This will be set to "initialized" if everything succeeds. + TranslationsPanelShared.#langListsInitState.set(panel, "error"); break; default: throw new Error( @@ -88,18 +144,28 @@ export class TranslationsPanelShared { await lazy.TranslationsParent.getSupportedLanguages(); // Verify that we are in a proper state. - if (languagePairs.length === 0) { + if (languagePairs.length === 0 || this.#simulateLangListError) { + this.#simulateLangListError = false; throw new Error("No translation languages were retrieved."); } - const fromPopups = panel.querySelectorAll( + const fromPopups = panelElement.querySelectorAll( ".translations-panel-language-menupopup-from" ); - const toPopups = panel.querySelectorAll( + const toPopups = panelElement.querySelectorAll( ".translations-panel-language-menupopup-to" ); for (const popup of fromPopups) { + // For the moment, the FullPageTranslationsPanel includes its own + // menu item for "Choose another language" as the first item in the list + // with an empty-string for its value. The SelectTranslationsPanel has + // only languages in its list with BCP-47 tags for values. As such, + // this loop works for both panels, to remove all of the languages + // from the list, but ensuring that any empty-string items are retained. + while (popup.lastChild?.value) { + popup.lastChild.remove(); + } for (const { langTag, displayName } of fromLanguages) { const fromMenuItem = document.createXULElement("menuitem"); fromMenuItem.setAttribute("value", langTag); @@ -109,6 +175,9 @@ export class TranslationsPanelShared { } for (const popup of toPopups) { + while (popup.lastChild?.value) { + popup.lastChild.remove(); + } for (const { langTag, displayName } of toLanguages) { const toMenuItem = document.createXULElement("menuitem"); toMenuItem.setAttribute("value", langTag); @@ -117,6 +186,6 @@ export class TranslationsPanelShared { } } - TranslationsPanelShared.#langListsInitState.set(id, "initialized"); + TranslationsPanelShared.#langListsInitState.set(panel, "initialized"); } } diff --git a/browser/components/translations/content/fullPageTranslationsPanel.js b/browser/components/translations/content/fullPageTranslationsPanel.js index 2e35440160..eddd3566f1 100644 --- a/browser/components/translations/content/fullPageTranslationsPanel.js +++ b/browser/components/translations/content/fullPageTranslationsPanel.js @@ -188,12 +188,19 @@ class CheckboxPageAction { } /** - * This singleton class controls the Translations popup panel. + * This singleton class controls the FullPageTranslations panel. * * This component is a `/browser` component, and the actor is a `/toolkit` actor, so care * must be taken to keep the presentation (this component) from the state management * (the Translations actor). This class reacts to state changes coming from the * Translations actor. + * + * A global instance of this class is created once per top ChromeWindow and is initialized + * when the new window is created. + * + * See the comment above TranslationsParent for more details. + * + * @see TranslationsParent */ var FullPageTranslationsPanel = new (class { /** @type {Console?} */ @@ -374,21 +381,6 @@ var FullPageTranslationsPanel = new (class { } /** - * @returns {TranslationsParent} - */ - #getTranslationsActor() { - const actor = - gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor( - "Translations" - ); - - if (!actor) { - throw new Error("Unable to get the TranslationsParent"); - } - return actor; - } - - /** * Fetches the language tags for the document and the user and caches the results * Use `#getCachedDetectedLanguages` when the lang tags do not need to be re-fetched. * This requires a bit of work to do, so prefer the cached version when possible. @@ -396,8 +388,9 @@ var FullPageTranslationsPanel = new (class { * @returns {Promise<LangTags>} */ async #fetchDetectedLanguages() { - this.detectedLanguages = - await this.#getTranslationsActor().getDetectedLanguages(); + this.detectedLanguages = await TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ).getDetectedLanguages(); return this.detectedLanguages; } @@ -421,11 +414,7 @@ var FullPageTranslationsPanel = new (class { */ async #ensureLangListsBuilt() { try { - await TranslationsPanelShared.ensureLangListsBuilt( - document, - this.elements.panel, - gBrowser.selectedBrowser.innerWindowID - ); + await TranslationsPanelShared.ensureLangListsBuilt(document, this); } catch (error) { this.console?.error(error); } @@ -438,7 +427,9 @@ var FullPageTranslationsPanel = new (class { * @param {TranslationsLanguageState} languageState */ #updateViewFromTranslationStatus( - languageState = this.#getTranslationsActor().languageState + languageState = TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ).languageState ) { const { translateButton, toMenuList, fromMenuList, header, cancelButton } = this.elements; @@ -553,7 +544,7 @@ var FullPageTranslationsPanel = new (class { // Unconditionally hide the intro text in case the panel is re-shown. intro.hidden = true; - if (TranslationsPanelShared.getLangListsInitState(panel) === "error") { + if (TranslationsPanelShared.getLangListsInitState(this) === "error") { // There was an error, display it in the view rather than the language // dropdowns. const { cancelButton, errorHintAction } = this.elements; @@ -722,8 +713,9 @@ var FullPageTranslationsPanel = new (class { const neverTranslateSiteMenuItems = panel.ownerDocument.querySelectorAll( ".never-translate-site-menuitem" ); - const neverTranslateSite = - await this.#getTranslationsActor().shouldNeverTranslateSite(); + const neverTranslateSite = await TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ).shouldNeverTranslateSite(); for (const menuitem of neverTranslateSiteMenuItems) { menuitem.setAttribute("checked", neverTranslateSite ? "true" : "false"); @@ -801,7 +793,9 @@ var FullPageTranslationsPanel = new (class { async #showRevisitView({ fromLanguage, toLanguage }) { const { fromMenuList, toMenuList, intro } = this.elements; if (!this.#isShowingDefaultView()) { - await this.#showDefaultView(this.#getTranslationsActor()); + await this.#showDefaultView( + TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser) + ); } intro.hidden = true; fromMenuList.value = fromLanguage; @@ -897,7 +891,7 @@ var FullPageTranslationsPanel = new (class { PanelMultiView.hidePopup(panel); await this.#showDefaultView( - this.#getTranslationsActor(), + TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser), true /* force this view to be shown */ ); @@ -1119,8 +1113,10 @@ var FullPageTranslationsPanel = new (class { const { button } = this.buttonElements; - const { requestedTranslationPair, locationChangeId } = - this.#getTranslationsActor().languageState; + const { requestedTranslationPair } = + TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ).languageState; // Store this value because it gets modified when #showDefaultView is called below. const isFirstUserInteraction = !this._hasShownPanel; @@ -1132,7 +1128,9 @@ var FullPageTranslationsPanel = new (class { this.console?.error(error); }); } else { - await this.#showDefaultView(this.#getTranslationsActor()).catch(error => { + await this.#showDefaultView( + TranslationsParent.getTranslationsActor(gBrowser.selectedBrowser) + ).catch(error => { this.console?.error(error); }); } @@ -1145,16 +1143,6 @@ var FullPageTranslationsPanel = new (class { ? button : this.elements.appMenuButton; - if (!TranslationsParent.isActiveLocation(locationChangeId)) { - this.console?.log(`A translation panel open request was stale.`, { - locationChangeId, - newlocationChangeId: - this.#getTranslationsActor().languageState.locationChangeId, - currentURISpec: gBrowser.currentURI.spec, - }); - return; - } - this.console?.log(`Showing a translation panel`, gBrowser.currentURI.spec); await this.#openPanelPopup(targetButton, { @@ -1173,7 +1161,9 @@ var FullPageTranslationsPanel = new (class { */ #isTranslationsActive() { const { requestedTranslationPair } = - this.#getTranslationsActor().languageState; + TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ).languageState; return requestedTranslationPair !== null; } @@ -1183,7 +1173,9 @@ var FullPageTranslationsPanel = new (class { async onTranslate() { PanelMultiView.hidePopup(this.elements.panel); - const actor = this.#getTranslationsActor(); + const actor = TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ); actor.translate( this.elements.fromMenuList.value, this.elements.toMenuList.value, @@ -1205,7 +1197,7 @@ var FullPageTranslationsPanel = new (class { this.#updateSettingsMenuLanguageCheckboxStates(); this.#updateSettingsMenuSiteCheckboxStates(); const popup = button.ownerDocument.getElementById( - "translations-panel-settings-menupopup" + "full-page-translations-panel-settings-menupopup" ); popup.openPopup(button, "after_end"); } @@ -1331,8 +1323,9 @@ var FullPageTranslationsPanel = new (class { */ async onNeverTranslateSite() { const pageAction = this.getCheckboxPageActionFor().neverTranslateSite(); - const toggledOn = - await this.#getTranslationsActor().toggleNeverTranslateSitePermissions(); + const toggledOn = await TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ).toggleNeverTranslateSitePermissions(); TranslationsParent.telemetry().panel().onNeverTranslateSite(toggledOn); this.#updateSettingsMenuSiteCheckboxStates(); await this.#doPageAction(pageAction); @@ -1349,7 +1342,9 @@ var FullPageTranslationsPanel = new (class { throw new Error("Expected to have a document language tag."); } - this.#getTranslationsActor().restorePage(docLangTag); + TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ).restorePage(docLangTag); } /** diff --git a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml index 72e2bd7095..8c643ea3f6 100644 --- a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml +++ b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml @@ -4,99 +4,99 @@ <html:template id="template-select-translations-panel"> <panel id="select-translations-panel" - class="panel-no-padding translations-panel" + class="panel-no-padding translations-panel translations-panel-view" type="arrow" role="alertdialog" noautofocus="true" aria-labelledby="translations-panel-header" - orient="vertical"> - <panelmultiview id="select-translations-panel-multiview" mainViewId="select-translations-panel-view-default"> - <panelview id="select-translations-panel-view-default" - class="PanelUI-subView translations-panel-view" - role="document" - mainview-with-header="true" - has-custom-header="true"> - <hbox class="panel-header select-translations-panel-header"> - <html:h1 class="translations-panel-header-wrapper"> - <html:span id="select-translations-panel-header" data-l10n-id="select-translations-panel-header"> - </html:span> - </html:h1> - <hbox class="translations-panel-beta"> - <image id="select-translations-panel-beta-icon" - class="translations-panel-beta-icon"> - </image> - </hbox> - <toolbarbutton id="select-translations-panel-settings" - class="panel-info-button translations-panel-settings-gear-icon" - data-l10n-id="translations-panel-settings-button" - closemenu="none" /> - </hbox> - <vbox class="select-translations-panel-content"> - <hbox id="select-translations-panel-lang-selection"> - <vbox flex="1"> - <label id="select-translations-panel-from-label" - class="select-translations-panel-label" - data-l10n-id="select-translations-panel-from-label"> - </label> - <menulist id="select-translations-panel-from" - flex="1" - value="detect" - size="large" - data-l10n-id="translations-panel-choose-language" - aria-labelledby="translations-panel-from-label"> - <menupopup id="select-translations-panel-from-menupopup" - class="translations-panel-language-menupopup-from"> - <!-- The list of <menuitem> will be dynamically inserted. --> - </menupopup> - </menulist> - </vbox> - <vbox flex="1"> - <label id="select-translations-panel-to-label" - class="select-translations-panel-label" - data-l10n-id="select-translations-panel-to-label"> - </label> - <menulist id="select-translations-panel-to" - flex="1" - value="detect" - size="large" - data-l10n-id="translations-panel-choose-language" - aria-labelledby="translations-panel-to-label"> - <menupopup id="select-translations-panel-to-menupopup" - class="translations-panel-language-menupopup-to"> - <!-- The list of <menuitem> will be dynamically inserted. --> - </menupopup> - </menulist> - </vbox> - </hbox> + orient="vertical" + onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)" + onpopuphidden="SelectTranslationsPanel.handlePanelPopupHiddenEvent(event)"> + <hbox class="panel-header select-translations-panel-header"> + <html:h1 class="translations-panel-header-wrapper"> + <html:span id="select-translations-panel-header" data-l10n-id="select-translations-panel-header"> + </html:span> + </html:h1> + <hbox class="translations-panel-beta"> + <image id="select-translations-panel-beta-icon" + class="translations-panel-beta-icon"> + </image> + </hbox> + <toolbarbutton id="select-translations-panel-settings" + class="panel-info-button translations-panel-settings-gear-icon" + data-l10n-id="translations-panel-settings-button" + closemenu="none" /> + </hbox> + <vbox class="select-translations-panel-content"> + <hbox id="select-translations-panel-lang-selection"> + <vbox flex="1"> + <label id="select-translations-panel-from-label" + class="select-translations-panel-label" + data-l10n-id="select-translations-panel-from-label"> + </label> + <menulist id="select-translations-panel-from" + flex="1" + value="" + size="large" + data-l10n-id="translations-panel-choose-language" + aria-labelledby="select-translations-panel-from-label" + noinitialselection="true" + oncommand="SelectTranslationsPanel.onChangeFromLanguage(event)"> + <menupopup id="select-translations-panel-from-menupopup" + onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)" + class="translations-panel-language-menupopup-from"> + <!-- The list of <menuitem> will be dynamically inserted. --> + </menupopup> + </menulist> </vbox> - <vbox class="select-translations-panel-content"> - <html:textarea id="select-translations-panel-translation-area" - data-l10n-id="select-translations-panel-placeholder-text" - readonly="true" - tabindex="0"> - </html:textarea> + <vbox flex="1"> + <label id="select-translations-panel-to-label" + class="select-translations-panel-label" + data-l10n-id="select-translations-panel-to-label"> + </label> + <menulist id="select-translations-panel-to" + flex="1" + value="" + size="large" + data-l10n-id="translations-panel-choose-language" + aria-labelledby="select-translations-panel-to-label" + noinitialselection="true" + oncommand="SelectTranslationsPanel.onChangeToLanguage(event)"> + <menupopup id="select-translations-panel-to-menupopup" + onpopupshown="SelectTranslationsPanel.handlePanelPopupShownEvent(event)" + class="translations-panel-language-menupopup-to"> + <!-- The list of <menuitem> will be dynamically inserted. --> + </menupopup> + </menulist> </vbox> + </hbox> + </vbox> + <vbox class="select-translations-panel-content"> + <html:textarea id="select-translations-panel-text-area" + class="select-translations-panel-text-area" + readonly="true" + tabindex="0"> + </html:textarea> + </vbox> - <hbox class="select-translations-panel-content"> - <button id="select-translations-panel-copy-button" - class="footer-button select-translations-panel-button select-translations-panel-copy-button" - data-l10n-id="select-translations-panel-copy-button"> - </button> - </hbox> + <hbox class="select-translations-panel-content"> + <button id="select-translations-panel-copy-button" + class="footer-button select-translations-panel-button select-translations-panel-copy-button" + data-l10n-id="select-translations-panel-copy-button"> + </button> + </hbox> - <html:moz-button-group class="panel-footer translations-panel-footer"> - <button id="select-translations-panel-translate-full-page-button" - class="footer-button select-translations-panel-button" - data-l10n-id="select-translations-panel-translate-full-page-button"> - </button> - <button id="select-translations-panel-done-button" - class="footer-button select-translations-panel-button" - data-l10n-id="select-translations-panel-done-button" - default="true" - oncommand = "SelectTranslationsPanel.close()"> - </button> - </html:moz-button-group> - </panelview> - </panelmultiview> + <html:moz-button-group class="panel-footer translations-panel-footer"> + <button id="select-translations-panel-translate-full-page-button" + class="footer-button select-translations-panel-button" + data-l10n-id="select-translations-panel-translate-full-page-button"> + </button> + <button id="select-translations-panel-done-button" + class="footer-button select-translations-panel-button" + data-l10n-id="select-translations-panel-done-button" + default="true" + oncommand = "SelectTranslationsPanel.close()"> + </button> + </html:moz-button-group> </panel> </html:template> diff --git a/browser/components/translations/content/selectTranslationsPanel.js b/browser/components/translations/content/selectTranslationsPanel.js index b4fe3e9735..bb825eaefa 100644 --- a/browser/components/translations/content/selectTranslationsPanel.js +++ b/browser/components/translations/content/selectTranslationsPanel.js @@ -4,15 +4,27 @@ /* eslint-env mozilla/browser-window */ +/** + * @typedef {import("../../../../toolkit/components/translations/translations").SelectTranslationsPanelState} SelectTranslationsPanelState + */ + ChromeUtils.defineESModuleGetters(this, { LanguageDetector: "resource://gre/modules/translation/LanguageDetector.sys.mjs", TranslationsPanelShared: "chrome://browser/content/translations/TranslationsPanelShared.sys.mjs", + Translator: "chrome://global/content/translations/Translator.mjs", }); /** - * This singleton class controls the Translations popup panel. + * This singleton class controls the SelectTranslations panel. + * + * A global instance of this class is created once per top ChromeWindow and is initialized + * when the context menu is opened in that window. + * + * See the comment above TranslationsParent for more details. + * + * @see TranslationsParent */ var SelectTranslationsPanel = new (class { /** @type {Console?} */ @@ -40,6 +52,69 @@ var SelectTranslationsPanel = new (class { } /** + * The textarea height for shorter text. + * + * @type {string} + */ + #shortTextHeight = "8em"; + + /** + * Retrieves the read-only textarea height for shorter text. + * + * @see #shortTextHeight + */ + get shortTextHeight() { + return this.#shortTextHeight; + } + + /** + * The textarea height for shorter text. + * + * @type {string} + */ + #longTextHeight = "16em"; + + /** + * Retrieves the read-only textarea height for longer text. + * + * @see #longTextHeight + */ + get longTextHeight() { + return this.#longTextHeight; + } + + /** + * The threshold used to determine when the panel should + * use the short text-height vs. the long-text height. + * + * @type {number} + */ + #textLengthThreshold = 800; + + /** + * Retrieves the read-only text-length threshold. + * + * @see #textLengthThreshold + */ + get textLengthThreshold() { + return this.#textLengthThreshold; + } + + /** + * The localized placeholder text to display when idle. + * + * @type {string} + */ + #idlePlaceholderText; + + /** + * The localized placeholder text to display when translating. + * + * @type {string} + */ + #translatingPlaceholderText; + + /** * Where the lazy elements are stored. * * @type {Record<string, Element>?} @@ -47,6 +122,29 @@ var SelectTranslationsPanel = new (class { #lazyElements; /** + * The internal state of the SelectTranslationsPanel. + * + * @type {SelectTranslationsPanelState} + */ + #translationState = { phase: "closed" }; + + /** + * The Translator for the current language pair. + * + * @type {Translator} + */ + #translator; + + /** + * An Id that increments with each translation, used to help keep track + * of whether an active translation request continue its progression or + * stop due to the existence of a newer translation request. + * + * @type {number} + */ + #translationId = 0; + + /** * Lazily creates the dom elements, and lazily selects them. * * @returns {Record<string, Element>} @@ -77,11 +175,12 @@ var SelectTranslationsPanel = new (class { doneButton: "select-translations-panel-done-button", fromLabel: "select-translations-panel-from-label", fromMenuList: "select-translations-panel-from", + fromMenuPopup: "select-translations-panel-from-menupopup", header: "select-translations-panel-header", - multiview: "select-translations-panel-multiview", - textArea: "select-translations-panel-translation-area", + textArea: "select-translations-panel-text-area", toLabel: "select-translations-panel-to-label", toMenuList: "select-translations-panel-to", + toMenuPopup: "select-translations-panel-to-menupopup", translateFullPageButton: "select-translations-panel-translate-full-page-button", }); @@ -91,6 +190,43 @@ var SelectTranslationsPanel = new (class { } /** + * Attempts to determine the best language tag to use as the source language for translation. + * If the detected language is not supported, attempts to fallback to the document's language tag. + * + * @param {string} textToTranslate - The text for which the language detection and target language retrieval are performed. + * + * @returns {Promise<string>} - The code of a supported language, a supported document language, or the top detected language. + */ + async getTopSupportedDetectedLanguage(textToTranslate) { + // First see if any of the detected languages are supported and return it if so. + const { language, languages } = await LanguageDetector.detectLanguage( + textToTranslate + ); + for (const { languageCode } of languages) { + const isSupported = await TranslationsParent.isSupportedAsFromLang( + languageCode + ); + if (isSupported) { + return languageCode; + } + } + + // Since none of the detected languages were supported, check to see if the + // document has a specified language tag that is supported. + const actor = TranslationsParent.getTranslationsActor( + gBrowser.selectedBrowser + ); + const detectedLanguages = actor.languageState.detectedLanguages; + if (detectedLanguages?.isDocLangTagSupported) { + return detectedLanguages.docLangTag; + } + + // No supported language was found, so return the top detected language + // to inform the panel's unsupported language state. + return language; + } + + /** * Detects the language of the provided text and retrieves a language pair for translation * based on user settings. * @@ -101,9 +237,7 @@ var SelectTranslationsPanel = new (class { */ async getLangPairPromise(textToTranslate) { const [fromLang, toLang] = await Promise.all([ - LanguageDetector.detectLanguage(textToTranslate).then( - ({ language }) => language - ), + SelectTranslationsPanel.getTopSupportedDetectedLanguage(textToTranslate), TranslationsParent.getTopPreferredSupportedToLang(), ]); @@ -122,99 +256,740 @@ var SelectTranslationsPanel = new (class { } /** - * Builds the <menulist> of languages for both the "from" and "to". This can be - * called every time the popup is shown, as it will retry when there is an error - * (such as a network error) or be a noop if it's already initialized. + * Ensures that the from-language and to-language dropdowns are built. + * + * This can be called every time the popup is shown, since it will retry + * when there is an error (such as a network error) or be a no-op if the + * dropdowns have already been initialized. */ async #ensureLangListsBuilt() { - try { - await TranslationsPanelShared.ensureLangListsBuilt( - document, - this.elements.panel - ); - } catch (error) { - this.console?.error(error); - } + await TranslationsPanelShared.ensureLangListsBuilt(document, this); } /** - * Updates the language dropdown based on the provided language tag. + * Initializes the selected value of the given language dropdown based on the language tag. * * @param {string} langTag - A BCP-47 language tag. - * @param {Element} menuList - The dropdown menu element that will be updated based on language support. + * @param {Element} menuList - The menu list element to update. + * * @returns {Promise<void>} */ - async #updateLanguageDropdown(langTag, menuList) { - const langTagIsSupported = + async #initializeLanguageMenuList(langTag, menuList) { + const isLangTagSupported = menuList.id === this.elements.fromMenuList.id ? await TranslationsParent.isSupportedAsFromLang(langTag) : await TranslationsParent.isSupportedAsToLang(langTag); - if (langTagIsSupported) { + if (isLangTagSupported) { // Remove the data-l10n-id because the menulist label will // be populated from the supported language's display name. - menuList.value = langTag; menuList.removeAttribute("data-l10n-id"); + menuList.value = langTag; } else { - // Set the data-l10n-id placeholder because no valid - // language will be selected when the panel opens. - menuList.value = undefined; - document.l10n.setAttributes( - menuList, - "translations-panel-choose-language" - ); - await document.l10n.translateElements([menuList]); + await this.#deselectLanguage(menuList); } } /** - * Updates the language selection dropdowns based on the given langPairPromise. + * Initializes the selected values of the from-language and to-language menu + * lists based on the result of the given language pair promise. * * @param {Promise<{fromLang?: string, toLang?: string}>} langPairPromise + * * @returns {Promise<void>} */ - async #updateLanguageDropdowns(langPairPromise) { + async #initializeLanguageMenuLists(langPairPromise) { const { fromLang, toLang } = await langPairPromise; - - this.console?.debug(`fromLang(${fromLang})`); - this.console?.debug(`toLang(${toLang})`); - const { fromMenuList, toMenuList } = this.elements; - await Promise.all([ - this.#updateLanguageDropdown(fromLang, fromMenuList), - this.#updateLanguageDropdown(toLang, toMenuList), + this.#initializeLanguageMenuList(fromLang, fromMenuList), + this.#initializeLanguageMenuList(toLang, toMenuList), ]); } /** - * Opens the panel and populates the currently selected fromLang and toLang based - * on the result of the langPairPromise. + * Opens the panel, ensuring the panel's UI and state are initialized correctly. * * @param {Event} event - The triggering event for opening the panel. + * @param {number} screenX - The x-axis location of the screen at which to open the popup. + * @param {number} screenY - The y-axis location of the screen at which to open the popup. + * @param {string} sourceText - The text to translate. * @param {Promise} langPairPromise - Promise resolving to language pair data for initializing dropdowns. + * * @returns {Promise<void>} */ - async open(event, langPairPromise) { - this.console?.log("Showing a translation panel."); + async open(event, screenX, screenY, sourceText, langPairPromise) { + if (this.#isOpen()) { + return; + } + this.#registerSourceText(sourceText); await this.#ensureLangListsBuilt(); - await this.#updateLanguageDropdowns(langPairPromise); - - // TODO(Bug 1878721) Rework the logic of where to open the panel. - // - // For the moment, the Select Translations panel opens at the - // AppMenu Button, but it will eventually need to open near - // to the selected content. - const appMenuButton = document.getElementById("PanelUI-menu-button"); - const { panel, textArea } = this.elements; - - panel.addEventListener("popupshown", () => textArea.focus(), { - once: true, + + await Promise.all([ + this.#cachePlaceholderText(), + this.#initializeLanguageMenuLists(langPairPromise), + ]); + + this.#displayIdlePlaceholder(); + this.#maybeRequestTranslation(); + await this.#openPopup(event, screenX, screenY); + } + + /** + * Opens a the panel popup at a location on the screen. + * + * @param {Event} event - The event that triggers the popup opening. + * @param {number} screenX - The x-axis location of the screen at which to open the popup. + * @param {number} screenY - The y-axis location of the screen at which to open the popup. + */ + async #openPopup(event, screenX, screenY) { + await window.ensureCustomElements("moz-button-group"); + + this.console?.log("Showing SelectTranslationsPanel"); + const { panel } = this.elements; + panel.openPopupAtScreen(screenX, screenY, /* isContextMenu */ false, event); + } + + /** + * Adds the source text to the translation state and adapts the size of the text area based + * on the length of the text. + * + * @param {string} sourceText - The text to translate. + * + * @returns {Promise<void>} + */ + #registerSourceText(sourceText) { + const { textArea } = this.elements; + this.#changeStateTo("idle", /* retainEntries */ false, { + sourceText, }); - await PanelMultiView.openPopup(panel, appMenuButton, { - position: "bottomright topright", - triggerEvent: event, - }).catch(error => this.console?.error(error)); + + if (sourceText.length < SelectTranslationsPanel.textLengthThreshold) { + textArea.style.height = SelectTranslationsPanel.shortTextHeight; + } else { + textArea.style.height = SelectTranslationsPanel.longTextHeight; + } + } + + /** + * Caches the localized text to use as placeholders. + */ + async #cachePlaceholderText() { + const [idleText, translatingText] = await document.l10n.formatValues([ + { id: "select-translations-panel-idle-placeholder-text" }, + { id: "select-translations-panel-translating-placeholder-text" }, + ]); + this.#idlePlaceholderText = idleText; + this.#translatingPlaceholderText = translatingText; + } + + /** + * Handles events when a popup is shown within the panel, including showing + * the panel itself. + * + * @param {Event} event - The event that triggered the popup to show. + */ + handlePanelPopupShownEvent(event) { + const { panel, fromMenuPopup, toMenuPopup } = this.elements; + switch (event.target.id) { + case panel.id: { + this.#updatePanelUIFromState(); + break; + } + case fromMenuPopup.id: { + this.#maybeTranslateOnEvents(["popuphidden"], fromMenuPopup); + break; + } + case toMenuPopup.id: { + this.#maybeTranslateOnEvents(["popuphidden"], toMenuPopup); + break; + } + } + } + + /** + * Handles events when a popup is closed within the panel, including closing + * the panel itself. + * + * @param {Event} event - The event that triggered the popup to close. + */ + handlePanelPopupHiddenEvent(event) { + const { panel } = this.elements; + switch (event.target.id) { + case panel.id: { + this.#changeStateToClosed(); + break; + } + } + } + + /** + * Handles events when the panels select from-language is changed. + */ + onChangeFromLanguage() { + const { fromMenuList, toMenuList } = this.elements; + this.#maybeTranslateOnEvents(["blur", "keypress"], fromMenuList); + this.#maybeStealLanguageFrom(toMenuList); + } + + /** + * Handles events when the panels select to-language is changed. + */ + onChangeToLanguage() { + const { toMenuList, fromMenuList } = this.elements; + this.#maybeTranslateOnEvents(["blur", "keypress"], toMenuList); + this.#maybeStealLanguageFrom(fromMenuList); + } + + /** + * Clears the selected language and ensures that the menu list displays + * the proper placeholder text. + * + * @param {Element} menuList - The target menu list element to update. + */ + async #deselectLanguage(menuList) { + menuList.value = ""; + document.l10n.setAttributes(menuList, "translations-panel-choose-language"); + await document.l10n.translateElements([menuList]); + } + + /** + * Deselects the language from the target menu list if both menu lists + * have the same language selected, simulating the effect of one menu + * list stealing the selected language value from the other. + * + * @param {Element} menuList - The target menu list element to update. + */ + async #maybeStealLanguageFrom(menuList) { + const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); + if (fromLanguage === toLanguage) { + await this.#deselectLanguage(menuList); + this.#maybeFocusMenuList(menuList); + } + } + + /** + * Focuses on the given menu list if provided and empty, or defaults to focusing one + * of the from-menu or to-menu lists if either is empty. + * + * @param {Element} [menuList] - The menu list to focus if specified. + */ + #maybeFocusMenuList(menuList) { + if (menuList && !menuList.value) { + menuList.focus({ focusVisible: true }); + return; + } + + const { fromMenuList, toMenuList } = this.elements; + if (!fromMenuList.value) { + fromMenuList.focus({ focusVisible: true }); + } else if (!toMenuList.value) { + toMenuList.focus({ focusVisible: true }); + } + } + + /** + * Focuses the translated-text area and sets its overflow to auto post-animation. + */ + #indicateTranslatedTextArea({ overflow }) { + const { textArea } = this.elements; + textArea.focus({ focusVisible: true }); + requestAnimationFrame(() => { + // We want to set overflow to auto as the final animation, because if it is + // set before the translated text is displayed, then the scrollTop will + // move to the bottom as the text is populated. + // + // Setting scrollTop = 0 on its own works, but it sometimes causes an animation + // of the text jumping from the bottom to the top. It looks a lot cleaner to + // disable overflow before rendering the text, then re-enable it after it renders. + requestAnimationFrame(() => { + textArea.style.overflow = overflow; + textArea.scrollTop = 0; + }); + }); + } + + /** + * Checks if the given language pair matches the panel's currently selected language pair. + * + * @param {string} fromLanguage - The from-language to compare. + * @param {string} toLanguage - The to-language to compare. + * + * @returns {boolean} - True if the given language pair matches the selected languages in the panel UI, otherwise false. + */ + #isSelectedLangPair(fromLanguage, toLanguage) { + const { fromLanguage: selectedFromLang, toLanguage: selectedToLang } = + this.#getSelectedLanguagePair(); + return fromLanguage === selectedFromLang && toLanguage === selectedToLang; + } + + /** + * Checks if the translator's language configuration matches the given language pair. + * + * @param {string} fromLanguage - The from-language to compare. + * @param {string} toLanguage - The to-language to compare. + * + * @returns {boolean} - True if the translator's languages match the given pair, otherwise false. + */ + #translatorMatchesLangPair(fromLanguage, toLanguage) { + return ( + this.#translator?.fromLanguage === fromLanguage && + this.#translator?.toLanguage === toLanguage + ); + } + + /** + * Retrieves the currently selected language pair from the menu lists. + * + * @returns {{fromLanguage: string, toLanguage: string}} An object containing the selected languages. + */ + #getSelectedLanguagePair() { + const { fromMenuList, toMenuList } = this.elements; + return { + fromLanguage: fromMenuList.value, + toLanguage: toMenuList.value, + }; + } + + /** + * Retrieves the source text from the translation state. + * This value is not available when the panel is closed. + * + * @returns {string | undefined} The source text. + */ + getSourceText() { + return this.#translationState?.sourceText; + } + + /** + * Retrieves the source text from the translation state. + * This value is only available in the translated phase. + * + * @returns {string | undefined} The translated text. + */ + getTranslatedText() { + return this.#translationState?.translatedText; + } + + /** + * Retrieves the current phase of the translation state. + * + * @returns {SelectTranslationsPanelState} + */ + #phase() { + return this.#translationState.phase; + } + + /** + * @returns {boolean} True if the panel is open, otherwise false. + */ + #isOpen() { + return this.#phase() !== "closed"; + } + + /** + * @returns {boolean} True if the panel is closed, otherwise false. + */ + #isClosed() { + return this.#phase() === "closed"; + } + + /** + * Changes the translation state to a new phase with options to retain or overwrite existing entries. + * + * @param {SelectTranslationsPanelState} phase - The new phase to transition to. + * @param {boolean} [retainEntries] - Whether to retain existing state entries that are not overwritten. + * @param {object | null} [data=null] - Additional data to merge into the state. + * @throws {Error} If an invalid phase is specified. + */ + #changeStateTo(phase, retainEntries, data = null) { + const { textArea } = this.elements; + switch (phase) { + case "translating": { + textArea.classList.add("translating"); + break; + } + case "closed": + case "idle": + case "translatable": + case "translated": { + textArea.classList.remove("translating"); + break; + } + default: { + throw new Error(`Invalid state change to '${phase}'`); + } + } + + const previousPhase = this.#phase(); + if (data && retainEntries) { + // Change the phase and apply new entries from data, but retain non-overwritten entries from previous state. + this.#translationState = { ...this.#translationState, phase, ...data }; + } else if (data) { + // Change the phase and apply new entries from data, but drop any entries that are not overwritten by data. + this.#translationState = { phase, ...data }; + } else if (retainEntries) { + // Change only the phase and retain all entries from previous data. + this.#translationState.phase = phase; + } else { + // Change the phase and delete all entries from previous data. + this.#translationState = { phase }; + } + + if (previousPhase === this.#phase()) { + // Do not continue on to update the UI because the phase didn't change. + return; + } + + const { fromLanguage, toLanguage } = this.#translationState; + this.console?.debug( + `SelectTranslationsPanel (${fromLanguage ? fromLanguage : "??"}-${ + toLanguage ? toLanguage : "??" + }) state change (${previousPhase} => ${phase})` + ); + + this.#updatePanelUIFromState(); + } + + /** + * Changes the phase to closed, discarding any entries in the translation state. + */ + #changeStateToClosed() { + this.#changeStateTo("closed", /* retainEntries */ false); + } + + /** + * Changes the phase from "translatable" to "translating". + * + * @throws {Error} If the current state is not "translatable". + */ + #changeStateToTranslating() { + const phase = this.#phase(); + if (phase !== "translatable") { + throw new Error(`Invalid state change (${phase} => translating)`); + } + this.#changeStateTo("translating", /* retainEntries */ true); + } + + /** + * Changes the phase from "translating" to "translated". + * + * @throws {Error} If the current state is not "translating". + */ + #changeStateToTranslated(translatedText) { + const phase = this.#phase(); + if (phase !== "translating") { + throw new Error(`Invalid state change (${phase} => translated)`); + } + this.#changeStateTo("translated", /* retainEntries */ true, { + translatedText, + }); + } + + /** + * Transitions the phase of the state based on the given language pair. + * + * @param {string} fromLanguage - The BCP-47 from-language tag. + * @param {string} toLanguage - The BCP-47 to-language tag. + * + * @returns {SelectTranslationsPanelState} The new phase of the translation state. + */ + #changeStateByLanguagePair(fromLanguage, toLanguage) { + const { + phase: previousPhase, + fromLanguage: previousFromLanguage, + toLanguage: previousToLanguage, + } = this.#translationState; + + let nextPhase = "translatable"; + + if ( + // No from-language is selected, so we cannot translate. + !fromLanguage || + // No to-language is selected, so we cannot translate. + !toLanguage || + // The same language has been selected, so we cannot translate. + fromLanguage === toLanguage + ) { + nextPhase = "idle"; + } else if ( + // The languages have not changed, so there is nothing to do. + previousFromLanguage === fromLanguage && + previousToLanguage === toLanguage + ) { + nextPhase = previousPhase; + } + + this.#changeStateTo(nextPhase, /* retainEntries */ true, { + fromLanguage, + toLanguage, + }); + + return nextPhase; + } + + /** + * Determines whether translation should continue based on panel state and language pair. + * + * @param {number} translationId - The id of the translation request to match. + * @param {string} fromLanguage - The from-language to analyze. + * @param {string} toLanguage - The to-language to analyze. + * + * @returns {boolean} True if translation should continue with the given pair, otherwise false. + */ + #shouldContinueTranslation(translationId, fromLanguage, toLanguage) { + return ( + // Continue only if the panel is still open. + this.#isOpen() && + // Continue only if the current translationId matches. + translationId === this.#translationId && + // Continue only if the given language pair is still the actively selected pair. + this.#isSelectedLangPair(fromLanguage, toLanguage) && + // Continue only if the given language pair matches the current translator. + this.#translatorMatchesLangPair(fromLanguage, toLanguage) + ); + } + + /** + * Displays the placeholder text for the translation state's "idle" phase. + */ + #displayIdlePlaceholder() { + const { textArea } = SelectTranslationsPanel.elements; + textArea.value = this.#idlePlaceholderText; + this.#updateTextDirection(); + this.#updateConditionalUIEnabledState(); + this.#maybeFocusMenuList(); + } + + /** + * Displays the placeholder text for the translation state's "translating" phase. + */ + #displayTranslatingPlaceholder() { + const { textArea } = SelectTranslationsPanel.elements; + textArea.value = this.#translatingPlaceholderText; + this.#updateTextDirection(); + this.#updateConditionalUIEnabledState(); + this.#indicateTranslatedTextArea({ overflow: "hidden" }); + } + + /** + * Displays the translated text for the translation state's "translated" phase. + */ + #displayTranslatedText() { + const { toLanguage } = this.#getSelectedLanguagePair(); + const { textArea } = SelectTranslationsPanel.elements; + textArea.value = this.getTranslatedText(); + this.#updateTextDirection(toLanguage); + this.#updateConditionalUIEnabledState(); + this.#indicateTranslatedTextArea({ overflow: "auto" }); + } + + /** + * Enables or disables UI components that are conditional on a valid language pair being selected. + */ + #updateConditionalUIEnabledState() { + const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); + const { copyButton, translateFullPageButton, textArea } = this.elements; + + const invalidLangPairSelected = !fromLanguage || !toLanguage; + const isTranslating = this.#phase() === "translating"; + + textArea.disabled = invalidLangPairSelected; + translateFullPageButton.disabled = invalidLangPairSelected; + copyButton.disabled = invalidLangPairSelected || isTranslating; + } + + /** + * Updates the panel UI based on the current phase of the translation state. + */ + #updatePanelUIFromState() { + switch (this.#phase()) { + case "idle": { + this.#displayIdlePlaceholder(); + break; + } + case "translating": { + this.#displayTranslatingPlaceholder(); + break; + } + case "translated": { + this.#displayTranslatedText(); + break; + } + } + } + + /** + * Sets the text direction attribute in the text areas based on the specified language. + * Uses the given language tag if provided, otherwise uses the current app locale. + * + * @param {string} [langTag] - The language tag to determine text direction. + */ + #updateTextDirection(langTag) { + const { textArea } = this.elements; + if (langTag) { + const scriptDirection = Services.intl.getScriptDirection(langTag); + textArea.setAttribute("dir", scriptDirection); + } else { + textArea.removeAttribute("dir"); + } + } + + /** + * Requests a translations port for a given language pair. + * + * @param {string} fromLanguage - The from-language. + * @param {string} toLanguage - The to-language. + * + * @returns {Promise<MessagePort | undefined>} The message port promise. + */ + async #requestTranslationsPort(fromLanguage, toLanguage) { + const innerWindowId = + gBrowser.selectedBrowser.browsingContext.top.embedderElement + .innerWindowID; + if (!innerWindowId) { + return undefined; + } + const port = await TranslationsParent.requestTranslationsPort( + innerWindowId, + fromLanguage, + toLanguage + ); + return port; + } + + /** + * Retrieves the existing translator for the specified language pair if it matches, + * otherwise creates a new translator. + * + * @param {string} fromLanguage - The source language code. + * @param {string} toLanguage - The target language code. + * + * @returns {Promise<Translator>} A promise that resolves to a `Translator` instance for the given language pair. + */ + async #getOrCreateTranslator(fromLanguage, toLanguage) { + if (this.#translatorMatchesLangPair(fromLanguage, toLanguage)) { + return this.#translator; + } + + this.console?.log( + `Creating new Translator (${fromLanguage}-${toLanguage})` + ); + if (this.#translator) { + this.#translator.destroy(); + this.#translator = null; + } + + this.#translator = await Translator.create( + fromLanguage, + toLanguage, + this.#requestTranslationsPort + ); + return this.#translator; + } + + /** + * Initiates the translation process if the panel state and selected languages + * meet the conditions for translation. + */ + #maybeRequestTranslation() { + if (this.#isClosed()) { + return; + } + const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair(); + const nextState = this.#changeStateByLanguagePair(fromLanguage, toLanguage); + if (nextState !== "translatable") { + return; + } + + const translationId = ++this.#translationId; + this.#getOrCreateTranslator(fromLanguage, toLanguage) + .then(translator => { + if ( + this.#shouldContinueTranslation( + translationId, + fromLanguage, + toLanguage + ) + ) { + this.#changeStateToTranslating(); + return translator.translate(this.getSourceText()); + } + return null; + }) + .then(translatedText => { + if ( + translatedText && + this.#shouldContinueTranslation( + translationId, + fromLanguage, + toLanguage + ) + ) { + this.#changeStateToTranslated(translatedText); + } else if (this.#isOpen()) { + this.#changeStateTo("idle", /* retainEntires */ false, { + sourceText: this.getSourceText(), + }); + } + }) + .catch(error => this.console?.error(error)); + } + + /** + * Attaches event listeners to the target element for initiating translation on specified event types. + * + * @param {string[]} eventTypes - An array of event types to listen for. + * @param {object} target - The target element to attach event listeners to. + * @throws {Error} If an unrecognized event type is provided. + */ + #maybeTranslateOnEvents(eventTypes, target) { + if (!target.translationListenerCallbacks) { + target.translationListenerCallbacks = []; + } + if (target.translationListenerCallbacks.length === 0) { + for (const eventType of eventTypes) { + let callback; + switch (eventType) { + case "blur": + case "popuphidden": { + callback = () => { + this.#maybeRequestTranslation(); + this.#removeTranslationListeners(target); + }; + break; + } + case "keypress": { + callback = event => { + if (event.key === "Enter") { + this.#maybeRequestTranslation(); + } + this.#removeTranslationListeners(target); + }; + break; + } + default: { + throw new Error( + `Invalid translation event type given: '${eventType}` + ); + } + } + target.addEventListener(eventType, callback, { once: true }); + target.translationListenerCallbacks.push({ eventType, callback }); + } + } + } + + /** + * Removes all translation event listeners from the target element. + * + * @param {Element} target - The element from which event listeners are to be removed. + */ + #removeTranslationListeners(target) { + for (const { eventType, callback } of target.translationListenerCallbacks) { + target.removeEventListener(eventType, callback); + } + target.translationListenerCallbacks = []; } })(); diff --git a/browser/components/translations/moz.build b/browser/components/translations/moz.build index 212b93e509..49f3afc632 100644 --- a/browser/components/translations/moz.build +++ b/browser/components/translations/moz.build @@ -3,7 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. with Files("**"): - BUG_COMPONENT = ("Firefox", "Translation") + BUG_COMPONENT = ("Firefox", "Translations") BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] diff --git a/browser/components/translations/tests/browser/browser.toml b/browser/components/translations/tests/browser/browser.toml index a9d36363da..472ae28866 100644 --- a/browser/components/translations/tests/browser/browser.toml +++ b/browser/components/translations/tests/browser/browser.toml @@ -15,6 +15,10 @@ support-files = [ ["browser_translations_about_preferences_settings_ui.js"] +["browser_translations_full_page_move_tab_to_new_window.js"] + +["browser_translations_full_page_multiple_windows.js"] + ["browser_translations_full_page_panel_a11y_focus.js"] ["browser_translations_full_page_panel_always_translate_language_bad_data.js"] @@ -51,8 +55,6 @@ support-files = [ ["browser_translations_full_page_panel_engine_unsupported.js"] -["browser_translations_full_page_panel_engine_unsupported_lang.js"] - ["browser_translations_full_page_panel_firstrun.js"] ["browser_translations_full_page_panel_firstrun_revisit.js"] @@ -62,6 +64,8 @@ skip-if = ["true"] ["browser_translations_full_page_panel_gear.js"] +["browser_translations_full_page_panel_init_failure.js"] + ["browser_translations_full_page_panel_never_translate_language.js"] ["browser_translations_full_page_panel_never_translate_site_auto.js"] @@ -77,6 +81,8 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227 ["browser_translations_full_page_panel_switch_languages.js"] +["browser_translations_full_page_panel_unsupported_lang.js"] + ["browser_translations_full_page_reader_mode.js"] ["browser_translations_full_page_telemetry_firstrun_auto_translate.js"] @@ -109,6 +115,30 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227 ["browser_translations_select_context_menu_with_text_selected.js"] -["browser_translations_select_panel_language_selectors.js"] +["browser_translations_select_panel_engine_cache.js"] + +["browser_translations_select_panel_fallback_to_doc_language.js"] + +["browser_translations_select_panel_open_to_idle_state.js"] + +["browser_translations_select_panel_retranslate_on_change_language_directly.js"] + +["browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js"] + +["browser_translations_select_panel_select_current_language_directly.js"] + +["browser_translations_select_panel_select_current_language_from_dropdown_menu.js"] + +["browser_translations_select_panel_select_same_from_and_to_languages_directly.js"] + +["browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js"] + +["browser_translations_select_panel_translate_on_change_language_directly.js"] + +["browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js"] + +["browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js"] + +["browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js"] -["browser_translations_select_panel_mainview_ui.js"] +["browser_translations_select_panel_translate_on_open.js"] diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js index ee81b84a36..f618b27814 100644 --- a/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js +++ b/browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js @@ -22,8 +22,8 @@ add_task(async function test_translations_settings_pane_elements() { translationsSettingsDescription, translateAlwaysHeader, translateNeverHeader, - translateAlwaysAddButton, - translateNeverAddButton, + translateAlwaysMenuList, + translateNeverMenuList, translateNeverSiteHeader, translateNeverSiteDesc, translateDownloadLanguagesHeader, @@ -41,8 +41,8 @@ add_task(async function test_translations_settings_pane_elements() { translationsSettingsDescription, translateAlwaysHeader, translateNeverHeader, - translateAlwaysAddButton, - translateNeverAddButton, + translateAlwaysMenuList, + translateNeverMenuList, translateNeverSiteHeader, translateNeverSiteDesc, translateDownloadLanguagesHeader, @@ -74,14 +74,203 @@ add_task(async function test_translations_settings_pane_elements() { translationsSettingsDescription, translateAlwaysHeader, translateNeverHeader, - translateAlwaysAddButton, - translateNeverAddButton, + translateAlwaysMenuList, + translateNeverMenuList, translateNeverSiteHeader, translateNeverSiteDesc, translateDownloadLanguagesHeader, translateDownloadLanguagesLearnMore, }, }); + await cleanup(); +}); + +add_task(async function test_translations_settings_always_translate() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + const { translateAlwaysMenuList } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + let alwaysTranslateSection = document.getElementById( + "translations-settings-always-translate-section" + ); + await testLanguageList(alwaysTranslateSection, translateAlwaysMenuList); + + await cleanup(); +}); + +async function testLanguageList(translateSection, menuList) { + const sectionName = + translateSection.id === "translations-settings-always-translate-section" + ? "Always" + : "Never"; + + is( + translateSection.querySelector(".translations-settings-languages-card"), + null, + `Language list not present in ${sectionName} Translate list` + ); + + for (let i = 0; i < menuList.children[0].children.length; i++) { + menuList.value = menuList.children[0].children[i].value; + + let clickMenu = BrowserTestUtils.waitForEvent(menuList, "command"); + menuList.dispatchEvent(new Event("command")); + await clickMenu; + + /** Languages are always added on the top, so check the firstChild + * for newly added languages. + * the firstChild.lastChild.innerText is the language display name + * which is compared with the menulist display name that is selected + */ + is( + translateSection.querySelector(".translations-settings-language-list") + .firstChild.lastChild.innerText, + getIntlDisplayName(menuList.children[0].children[i].value), + `Language list has element ${getIntlDisplayName( + menuList.children[0].children[i].value + )}` + ); + } + /** The test cases has 4 languages, so check if 4 languages are added to the list */ + let langNum = translateSection.querySelector( + ".translations-settings-language-list" + ).childElementCount; + is(langNum, 4, "Number of languages added is 4"); + + const languagelist = translateSection.querySelector( + ".translations-settings-language-list" + ); + + for (let i = 0; i < langNum; i++) { + // Delete the first language in the list + let langName = languagelist.children[0].lastChild.innerText; + let langButton = languagelist.children[0].querySelector("moz-button"); + + let clickButton = BrowserTestUtils.waitForEvent(langButton, "click"); + langButton.dispatchEvent(new Event("click")); + await clickButton; + + if (i < langNum - 1) { + is( + languagelist.childElementCount, + langNum - i - 1, + `${langName} removed from ${sectionName} Translate` + ); + } else { + /** Check if the language list card is removed after removing the last language */ + is( + translateSection.querySelector(".translations-settings-languages-card"), + null, + `${langName} removed from ${sectionName} Translate` + ); + } + } +} + +add_task(async function test_translations_settings_never_translate() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + + const document = gBrowser.selectedBrowser.contentDocument; + + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + const { translateNeverMenuList } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + let neverTranslateSection = document.getElementById( + "translations-settings-never-translate-section" + ); + await testLanguageList(neverTranslateSection, translateNeverMenuList); + await cleanup(); +}); + +add_task(async function test_translations_settings_download_languages() { + const { + cleanup, + elements: { settingsButton }, + } = await setupAboutPreferences(LANGUAGE_PAIRS, { + prefs: [["browser.translations.newSettingsUI.enable", true]], + }); + assertVisibility({ + message: "Expect paneGeneral elements to be visible.", + visible: { settingsButton }, + }); + + const { translateDownloadLanguagesList } = + await TranslationsSettingsTestUtils.openAboutPreferencesTranslationsSettingsPane( + settingsButton + ); + + let langList = translateDownloadLanguagesList.querySelector( + ".translations-settings-language-list" + ); + + for (let i = 0; i < langList.children.length; i++) { + is( + langList.children[i] + .querySelector("moz-button") + .classList.contains("translations-settings-download-icon"), + true, + "Download icon is visible" + ); + + let clickButton = BrowserTestUtils.waitForEvent( + langList.children[i].querySelector("moz-button"), + "click" + ); + langList.children[i] + .querySelector("moz-button") + .dispatchEvent(new Event("click")); + await clickButton; + + is( + langList.children[i] + .querySelector("moz-button") + .classList.contains("translations-settings-delete-icon"), + true, + "Delete icon is visible" + ); + + clickButton = BrowserTestUtils.waitForEvent( + langList.children[i].querySelector("moz-button"), + "click" + ); + langList.children[i] + .querySelector("moz-button") + .dispatchEvent(new Event("click")); + await clickButton; + + is( + langList.children[i] + .querySelector("moz-button") + .classList.contains("translations-settings-download-icon"), + true, + "Download icon is visible" + ); + } await cleanup(); }); diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_move_tab_to_new_window.js b/browser/components/translations/tests/browser/browser_translations_full_page_move_tab_to_new_window.js new file mode 100644 index 0000000000..f384fc59c8 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_full_page_move_tab_to_new_window.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests a specific situation described in Bug 1893776 + * where the Translations panels were not initializing correctly after + * dragging a tab to become its own new window after opening the panel + * in the previous window. + */ +add_task(async function test_browser_translations_full_page_multiple_windows() { + const window1 = window; + const testPage = await loadTestPage({ + page: SPANISH_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + }); + + await FullPageTranslationsTestUtils.assertTranslationsButton( + { button: true, circleArrows: false, locale: false, icon: true }, + "The translations button is visible.", + window1 + ); + + info("Opening FullPageTranslationsPanel in window1"); + await FullPageTranslationsTestUtils.openPanel({ + onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault, + }); + + await FullPageTranslationsTestUtils.clickCancelButton(); + + info("Moving the tab to a new window of its own"); + const window2 = await window1.gBrowser.replaceTabWithWindow(testPage.tab); + const swapDocShellPromise = BrowserTestUtils.waitForEvent( + testPage.tab.linkedBrowser, + "SwapDocShells" + ); + await swapDocShellPromise; + + await FullPageTranslationsTestUtils.assertTranslationsButton( + { button: true, circleArrows: false, locale: false, icon: true }, + "The translations button is visible.", + window2 + ); + + info("Opening FullPageTranslationsPanel in window2"); + await FullPageTranslationsTestUtils.openPanel({ + win: window2, + }); + + info("Translating the same page in window2"); + await FullPageTranslationsTestUtils.clickTranslateButton({ + win: window2, + downloadHandler: testPage.resolveDownloads, + }); + await FullPageTranslationsTestUtils.assertLangTagIsShownOnTranslationsButton( + "es", + "en", + window2 + ); + + await testPage.cleanup(); + await BrowserTestUtils.closeWindow(window2); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_multiple_windows.js b/browser/components/translations/tests/browser/browser_translations_full_page_multiple_windows.js new file mode 100644 index 0000000000..9bdee2c406 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_full_page_multiple_windows.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * @param {Window} win + */ +function focusWindow(win) { + const promise = BrowserTestUtils.waitForEvent(win, "focus"); + win.focus(); + return promise; +} + +/** + * Test that the full page translation panel works when multiple windows are used. + */ +add_task(async function test_browser_translations_full_page_multiple_windows() { + const window1 = window; + const testPage1 = await loadTestPage({ + page: SPANISH_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + }); + + const window2 = await BrowserTestUtils.openNewBrowserWindow(); + + const testPage2 = await loadTestPage({ + win: window2, + page: SPANISH_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + }); + + // Focus back to the original window first. This ensures coverage for invalid caching + // logic involving multiple windows. + await focusWindow(window1); + + info("Testing window 1"); + await FullPageTranslationsTestUtils.openPanel({ + onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault, + }); + await FullPageTranslationsTestUtils.clickTranslateButton({ + downloadHandler: testPage1.resolveDownloads, + }); + await FullPageTranslationsTestUtils.assertPageIsTranslated( + "es", + "en", + testPage1.runInPage, + "Window 1 gets translated", + window1 + ); + + await focusWindow(window2); + + info("Testing window 2"); + await FullPageTranslationsTestUtils.openPanel({ win: window2 }); + await FullPageTranslationsTestUtils.clickTranslateButton({ win: window2 }); + await FullPageTranslationsTestUtils.assertPageIsTranslated( + "es", + "en", + testPage2.runInPage, + "Window 2 gets translated", + window2 + ); + + await testPage2.cleanup(); + await BrowserTestUtils.closeWindow(window2); + await testPage1.cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_panel_init_failure.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_init_failure.js new file mode 100644 index 0000000000..34986726b8 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_init_failure.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies that the proper error message is displayed in + * the FullPageTranslationsPanel if the panel tries to open, but the language + * dropdown menus fail to initialize. + */ +add_task(async function test_full_page_translations_panel_init_failure() { + const { cleanup } = await loadTestPage({ + page: SPANISH_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + }); + + TranslationsPanelShared.simulateLangListError(); + await FullPageTranslationsTestUtils.openPanel({ + onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewInitFailure, + }); + + await FullPageTranslationsTestUtils.clickCancelButton(); + + await cleanup(); +}); diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_panel_retry.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_retry.js index 74d92381b9..01af5cbd8d 100644 --- a/browser/components/translations/tests/browser/browser_translations_full_page_panel_retry.js +++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_retry.js @@ -37,7 +37,7 @@ add_task(async function test_translations_panel_retry() { onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewRevisit, }); - FullPageTranslationsTestUtils.switchSelectedToLanguage("fr"); + FullPageTranslationsTestUtils.changeSelectedToLanguage("fr"); await FullPageTranslationsTestUtils.clickTranslateButton({ downloadHandler: resolveDownloads, diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_panel_switch_languages.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_switch_languages.js index 0c5db67b20..6ab70e634f 100644 --- a/browser/components/translations/tests/browser/browser_translations_full_page_panel_switch_languages.js +++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_switch_languages.js @@ -30,29 +30,29 @@ add_task(async function test_translations_panel_switch_language() { FullPageTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "es" }); FullPageTranslationsTestUtils.assertSelectedToLanguage({ langTag: "en" }); - FullPageTranslationsTestUtils.switchSelectedFromLanguage("en"); + FullPageTranslationsTestUtils.changeSelectedFromLanguage("en"); ok( translateButton.disabled, "The translate button is disabled when the languages are the same" ); - FullPageTranslationsTestUtils.switchSelectedFromLanguage("es"); + FullPageTranslationsTestUtils.changeSelectedFromLanguage("es"); ok( !translateButton.disabled, "When the languages are different it can be translated" ); - FullPageTranslationsTestUtils.switchSelectedFromLanguage(""); + FullPageTranslationsTestUtils.changeSelectedFromLanguage(""); ok( translateButton.disabled, "The translate button is disabled nothing is selected." ); - FullPageTranslationsTestUtils.switchSelectedFromLanguage("en"); - FullPageTranslationsTestUtils.switchSelectedToLanguage("fr"); + FullPageTranslationsTestUtils.changeSelectedFromLanguage("en"); + FullPageTranslationsTestUtils.changeSelectedToLanguage("fr"); ok(!translateButton.disabled, "The translate button can now be used"); diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_panel_engine_unsupported_lang.js b/browser/components/translations/tests/browser/browser_translations_full_page_panel_unsupported_lang.js index 21f7e8fdb7..59be1e329b 100644 --- a/browser/components/translations/tests/browser/browser_translations_full_page_panel_engine_unsupported_lang.js +++ b/browser/components/translations/tests/browser/browser_translations_full_page_panel_unsupported_lang.js @@ -23,6 +23,9 @@ add_task(async function test_unsupported_lang() { }); await FullPageTranslationsTestUtils.clickChangeSourceLanguageButton(); + FullPageTranslationsTestUtils.assertPanelViewDefault(); + FullPageTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "" }); + FullPageTranslationsTestUtils.assertSelectedToLanguage({ langTag: "en" }); await cleanup(); }); diff --git a/browser/components/translations/tests/browser/browser_translations_full_page_telemetry_switch_languages.js b/browser/components/translations/tests/browser/browser_translations_full_page_telemetry_switch_languages.js index ef13940b3f..41183cc9cf 100644 --- a/browser/components/translations/tests/browser/browser_translations_full_page_telemetry_switch_languages.js +++ b/browser/components/translations/tests/browser/browser_translations_full_page_telemetry_switch_languages.js @@ -24,7 +24,7 @@ add_task(async function test_translations_telemetry_switch_from_language() { }); FullPageTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "es" }); - FullPageTranslationsTestUtils.switchSelectedFromLanguage("en"); + FullPageTranslationsTestUtils.changeSelectedFromLanguage("en"); await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, { expectedEventCount: 1, @@ -45,7 +45,7 @@ add_task(async function test_translations_telemetry_switch_from_language() { } ); - FullPageTranslationsTestUtils.switchSelectedFromLanguage("es"); + FullPageTranslationsTestUtils.changeSelectedFromLanguage("es"); await TestTranslationsTelemetry.assertEvent( Glean.translationsPanel.changeFromLanguage, @@ -56,7 +56,7 @@ add_task(async function test_translations_telemetry_switch_from_language() { } ); - FullPageTranslationsTestUtils.switchSelectedFromLanguage(""); + FullPageTranslationsTestUtils.changeSelectedFromLanguage(""); await TestTranslationsTelemetry.assertEvent( Glean.translationsPanel.changeFromLanguage, @@ -65,7 +65,7 @@ add_task(async function test_translations_telemetry_switch_from_language() { } ); - FullPageTranslationsTestUtils.switchSelectedFromLanguage("en"); + FullPageTranslationsTestUtils.changeSelectedFromLanguage("en"); await TestTranslationsTelemetry.assertEvent( Glean.translationsPanel.changeFromLanguage, @@ -100,7 +100,7 @@ add_task(async function test_translations_telemetry_switch_to_language() { }); FullPageTranslationsTestUtils.assertSelectedToLanguage({ langTag: "en" }); - FullPageTranslationsTestUtils.switchSelectedToLanguage("fr"); + FullPageTranslationsTestUtils.changeSelectedToLanguage("fr"); await TestTranslationsTelemetry.assertEvent(Glean.translationsPanel.open, { expectedEventCount: 1, @@ -121,7 +121,7 @@ add_task(async function test_translations_telemetry_switch_to_language() { } ); - FullPageTranslationsTestUtils.switchSelectedToLanguage("en"); + FullPageTranslationsTestUtils.changeSelectedToLanguage("en"); await TestTranslationsTelemetry.assertEvent( Glean.translationsPanel.changeToLanguage, @@ -132,7 +132,7 @@ add_task(async function test_translations_telemetry_switch_to_language() { } ); - FullPageTranslationsTestUtils.switchSelectedToLanguage(""); + FullPageTranslationsTestUtils.changeSelectedToLanguage(""); await TestTranslationsTelemetry.assertEvent( Glean.translationsPanel.changeToLanguage, @@ -141,7 +141,7 @@ add_task(async function test_translations_telemetry_switch_to_language() { } ); - FullPageTranslationsTestUtils.switchSelectedToLanguage("en"); + FullPageTranslationsTestUtils.changeSelectedToLanguage("en"); await TestTranslationsTelemetry.assertEvent( Glean.translationsPanel.changeToLanguage, diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js index a6b3f71924..c3ed228ecc 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js +++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js @@ -19,7 +19,7 @@ add_task( async function test_translate_selection_menuitem_is_unavailable_with_feature_disabled_and_no_text_selected() { const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", false]], }); @@ -34,8 +34,8 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: false, - openAtSpanishParagraph: true, + selectSpanishSentence: false, + openAtSpanishSentence: true, expectMenuItemVisible: false, }, "The translate-selection context menu item should be unavailable when the feature is disabled." @@ -54,7 +54,7 @@ add_task( add_task( async function test_translate_selection_menuitem_is_unavailable_with_feature_disabled_and_text_selected() { const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", false]], }); @@ -69,8 +69,8 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: true, - openAtSpanishParagraph: true, + selectSpanishSentence: true, + openAtSpanishSentence: true, expectMenuItemVisible: false, }, "The translate-selection context menu item should be unavailable when the feature is disabled." @@ -89,7 +89,7 @@ add_task( add_task( async function test_translate_selection_menuitem_is_unavailable_with_feature_disabled_and_clicking_a_hyperlink() { const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", false]], }); @@ -102,7 +102,7 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: false, + selectSpanishSentence: false, openAtSpanishHyperlink: true, expectMenuItemVisible: false, }, diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js index 99cff2b4ec..788ca7de63 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js +++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js @@ -12,7 +12,7 @@ add_task( async function test_translate_selection_menuitem_with_text_selected_and_full_page_translations_active() { const { cleanup, resolveDownloads, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); @@ -27,8 +27,8 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: true, - openAtSpanishParagraph: true, + selectSpanishSentence: true, + openAtSpanishSentence: true, expectMenuItemVisible: true, expectedTargetLanguage: "en", }, @@ -52,8 +52,8 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: true, - openAtSpanishParagraph: true, + selectSpanishSentence: true, + openAtSpanishSentence: true, expectMenuItemVisible: false, }, "The translate-selection context menu item should be unavailable while full-page translations is active." @@ -70,8 +70,8 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: true, - openAtSpanishParagraph: true, + selectSpanishSentence: true, + openAtSpanishSentence: true, expectMenuItemVisible: true, expectedTargetLanguage: "en", }, @@ -91,7 +91,7 @@ add_task( add_task( async function test_translate_selection_menuitem_with_link_clicked_and_full_page_translations_active() { const { cleanup, resolveDownloads, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); @@ -106,7 +106,7 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: false, + selectSpanishSentence: false, openAtSpanishHyperlink: true, expectMenuItemVisible: true, expectedTargetLanguage: "en", @@ -131,7 +131,7 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: false, + selectSpanishSentence: false, openAtSpanishHyperlink: true, expectMenuItemVisible: false, }, @@ -149,7 +149,7 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: false, + selectSpanishSentence: false, openAtSpanishHyperlink: true, expectMenuItemVisible: true, expectedTargetLanguage: "en", diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js index cefd83f046..83e836489f 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js +++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js @@ -12,7 +12,7 @@ add_task( async function test_translate_selection_menuitem_translate_link_text_to_target_language() { const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); @@ -25,7 +25,7 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: false, + selectSpanishSentence: false, openAtSpanishHyperlink: true, expectMenuItemVisible: true, expectedTargetLanguage: "en", @@ -47,7 +47,7 @@ add_task( add_task( async function test_translate_selection_menuitem_translate_link_text_in_preferred_language() { const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); @@ -60,7 +60,7 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: false, + selectSpanishSentence: false, openAtEnglishHyperlink: true, expectMenuItemVisible: true, expectedTargetLanguage: null, @@ -82,7 +82,7 @@ add_task( add_task( async function test_translate_selection_menuitem_selected_text_takes_precedence_over_link_text() { const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); @@ -95,7 +95,7 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: true, + selectSpanishSentence: true, openAtEnglishHyperlink: true, expectMenuItemVisible: true, expectedTargetLanguage: "en", diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js index 82e5d3ba63..5e7d482441 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js +++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js @@ -10,7 +10,7 @@ add_task( async function test_translate_selection_menuitem_is_unavailable_when_no_text_is_selected() { const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); @@ -25,8 +25,8 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: false, - openAtSpanishParagraph: true, + selectSpanishSentence: false, + openAtSpanishSentence: true, expectMenuItemVisible: false, }, "The translate-selection context menu item should be unavailable when no text is selected." diff --git a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js index deb5911a37..6b44f2ca1f 100644 --- a/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js +++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js @@ -12,7 +12,7 @@ add_task( async function test_translate_selection_menuitem_when_selected_text_is_not_preferred_language() { const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); @@ -27,8 +27,8 @@ add_task( await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectSpanishParagraph: true, - openAtSpanishParagraph: true, + selectSpanishSentence: true, + openAtSpanishSentence: true, expectMenuItemVisible: true, expectedTargetLanguage: "en", }, @@ -49,21 +49,21 @@ add_task( add_task( async function test_translate_selection_menuitem_when_selected_text_is_preferred_language() { const { cleanup, runInPage } = await loadTestPage({ - page: ENGLISH_PAGE_URL, + page: SELECT_TEST_PAGE_URL, languagePairs: LANGUAGE_PAIRS, prefs: [["browser.translations.select.enable", true]], }); await FullPageTranslationsTestUtils.assertTranslationsButton( - { button: false }, + { button: true, circleArrows: false, locale: false, icon: true }, "The button is available." ); await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, { - selectFirstParagraph: true, - openAtFirstParagraph: true, + selectEnglishSentence: true, + openAtEnglishSentence: true, expectMenuItemVisible: true, expectedTargetLanguage: null, }, diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_engine_cache.js b/browser/components/translations/tests/browser/browser_translations_select_panel_engine_cache.js new file mode 100644 index 0000000000..a0ef58c694 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_engine_cache.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests that the SelectTranslationsPanel successfully + * caches the engine within the Translator for the given language pair, + * and if that engine is destroyed, the Translator will correctly reinitialize + * the engine, even for the same language pair. + */ +add_task( + async function test_select_translations_panel_translate_sentence_on_open() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSection: true, + openAtFrenchSection: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + expectedDownloads: 1, + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + // No downloads because the engine is cached for this language pair. + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + info("Explicitly destroying the Translations Engine."); + await destroyTranslationsEngine(); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + openAtFrenchHyperlink: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + // Expect downloads again since the engine was destroyed. + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_fallback_to_doc_language.js b/browser/components/translations/tests/browser/browser_translations_select_panel_fallback_to_doc_language.js new file mode 100644 index 0000000000..d2c6f42486 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_fallback_to_doc_language.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests the case of opening the SelectTranslationsPanel when the + * detected language is unsupported, but the page language is known to be a supported + * language. The panel should automatically fall back to the page language in an + * effort to combat falsely identified selections. + */ +add_task( + async function test_select_translations_panel_translate_sentence_on_open() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include French. + { fromLang: "es", toLang: "en" }, + { fromLang: "en", toLang: "es" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + // French is not supported, but the page is in Spanish, so expect Spanish. + expectedFromLanguage: "es", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_language_selectors.js b/browser/components/translations/tests/browser/browser_translations_select_panel_language_selectors.js deleted file mode 100644 index 1dcc76450f..0000000000 --- a/browser/components/translations/tests/browser/browser_translations_select_panel_language_selectors.js +++ /dev/null @@ -1,54 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -add_task( - async function test_select_translations_panel_open_spanish_language_selectors() { - const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, - languagePairs: LANGUAGE_PAIRS, - prefs: [["browser.translations.select.enable", true]], - }); - - await SelectTranslationsTestUtils.openPanel(runInPage, { - selectSpanishParagraph: true, - openAtSpanishParagraph: true, - expectedTargetLanguage: "en", - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewDefault, - }); - - SelectTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "es" }); - SelectTranslationsTestUtils.assertSelectedToLanguage({ langTag: "en" }); - - await SelectTranslationsTestUtils.clickDoneButton(); - - await cleanup(); - } -); - -add_task( - async function test_select_translations_panel_open_english_language_selectors() { - const { cleanup, runInPage } = await loadTestPage({ - page: ENGLISH_PAGE_URL, - languagePairs: LANGUAGE_PAIRS, - prefs: [["browser.translations.select.enable", true]], - }); - - await SelectTranslationsTestUtils.openPanel(runInPage, { - selectFirstParagraph: true, - openAtFirstParagraph: true, - expectedTargetLanguage: "en", - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewDefault, - }); - - SelectTranslationsTestUtils.assertSelectedFromLanguage({ langTag: "en" }); - SelectTranslationsTestUtils.assertSelectedToLanguage({ - l10nId: "translations-panel-choose-language", - }); - - await SelectTranslationsTestUtils.clickDoneButton(); - - await cleanup(); - } -); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_mainview_ui.js b/browser/components/translations/tests/browser/browser_translations_select_panel_mainview_ui.js deleted file mode 100644 index 79d21e57d0..0000000000 --- a/browser/components/translations/tests/browser/browser_translations_select_panel_mainview_ui.js +++ /dev/null @@ -1,36 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -/** - * This test case verifies the visibility and initial state of UI elements within the - * Select Translations Panel's main-view UI. - */ -add_task( - async function test_select_translations_panel_mainview_ui_element_visibility() { - const { cleanup, runInPage } = await loadTestPage({ - page: SPANISH_PAGE_URL, - languagePairs: LANGUAGE_PAIRS, - prefs: [["browser.translations.select.enable", true]], - }); - - await FullPageTranslationsTestUtils.assertTranslationsButton( - { button: true, circleArrows: false, locale: false, icon: true }, - "The button is available." - ); - - await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage); - - await SelectTranslationsTestUtils.openPanel(runInPage, { - selectSpanishParagraph: true, - openAtSpanishParagraph: true, - expectedTargetLanguage: "es", - onOpenPanel: SelectTranslationsTestUtils.assertPanelViewDefault, - }); - - await SelectTranslationsTestUtils.clickDoneButton(); - - await cleanup(); - } -); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js b/browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js new file mode 100644 index 0000000000..d5a1096e70 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_open_to_idle_state.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the select translations panel's functionality when opened with an unsupported + * from-language, ensuring it opens with the correct view with no from-language selected. + */ +add_task( + async function test_select_translations_panel_open_no_selected_from_lang() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + expectedFromLanguage: null, + expectedToLanguage: "en", + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case verifies the select translations panel's functionality when opened with an undetermined + * to-language, ensuring it opens with the correct view with no to-language selected. + */ +add_task( + async function test_select_translations_panel_open_no_selected_to_lang() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectEnglishSentence: true, + openAtEnglishSentence: true, + expectedFromLanguage: "en", + expectedToLanguage: null, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_directly.js new file mode 100644 index 0000000000..fff0326f75 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_directly.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of triggering a translation by directly switching + * the from-language when the panel is already in the "translated" state from a previous + * language pair. + */ +add_task( + async function test_select_translations_panel_retranslate_on_change_from_language_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], { + openDropdownMenu: false, + pivotTranslation: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of triggering a translation by directly switching + * the to-language when the panel is already in the "translated" state from a previous + * language pair. + */ +add_task( + async function test_select_translations_panel_retranslate_on_change_to_language_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], { + openDropdownMenu: false, + pivotTranslation: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js new file mode 100644 index 0000000000..16f2cb39f7 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of triggering a translation by switching the + * from-language by opening the language dropdown menu when the panel is already in + * the "translated" state from a previous language pair. + */ +add_task( + async function test_select_translations_panel_retranslate_on_change_from_language_via_popup() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + openAtSpanishHyperlink: true, + expectedFromLanguage: "es", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], { + openDropdownMenu: true, + pivotTranslation: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of triggering a translation by switching the + * to-language by opening the language dropdown menu when the panel is already in + * the "translated" state from a previous language pair. + */ +add_task( + async function test_select_translations_panel_retranslate_on_change_to_language_via_popup() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + openAtSpanishHyperlink: true, + expectedFromLanguage: "es", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], { + openDropdownMenu: true, + pivotTranslation: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_directly.js new file mode 100644 index 0000000000..f45326800f --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_directly.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of directly switching the from-language to the same + * from-language that is already selected, ensuring no change occurs to the translation state, + * and that no re-translation is triggered. + */ +add_task( + async function test_select_translations_panel_select_current_from_language_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + expectedFromLanguage: "es", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], { + openDropdownMenu: false, + // No downloads are resolved, because no re-translation is triggered. + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of directly switching the to-language to the same + * to-language that is already selected, ensuring no change occurs to the translation state, + * and that no re-translation is triggered. + */ +add_task( + async function test_select_translations_panel_select_current_from_language_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + openAtFrenchHyperlink: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], { + openDropdownMenu: false, + // No downloads are resolved, because no re-translation is triggered. + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_from_dropdown_menu.js new file mode 100644 index 0000000000..04aa731cf2 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_from_dropdown_menu.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of directly switching the from-language to the same + * from-language that is already selected by opening the language dropdown menu, + * ensuring no change occurs to the translation state, and that no re-translation is triggered. + */ +add_task( + async function test_select_translations_panel_select_current_from_language_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + expectedFromLanguage: "es", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], { + openDropdownMenu: true, + // No downloads are resolved, because no re-translation is triggered. + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of directly switching the to-language to the same + * to-language that is already selected by opening the language dropdown menu, + * ensuring no change occurs to the translation state, and that no re-translation is triggered. + */ +add_task( + async function test_select_translations_panel_select_current_from_language_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + openAtFrenchHyperlink: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], { + openDropdownMenu: true, + // No downloads are resolved, because no re-translation is triggered. + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js new file mode 100644 index 0000000000..95feac6708 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of switching the from-language to the same value + * that is currently selected in the to-language, effectively stealing the to-language's + * value, leaving it unselected and focused. + */ +add_task( + async function test_select_translations_panel_select_same_from_language_directly() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSection: true, + openAtSpanishSection: true, + expectedFromLanguage: null, + expectedToLanguage: "en", + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["en"], { + openDropdownMenu: false, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewNoFromToSelected, + }); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of switching the to-language to the same value + * that is currently selected in the from-language, effectively stealing the from-language's + * value, leaving it unselected and focused. + */ +add_task( + async function test_select_translations_panel_select_same_to_language_directly() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectEnglishSection: true, + openAtEnglishSection: true, + expectedFromLanguage: "en", + expectedToLanguage: null, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], { + openDropdownMenu: false, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + }); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js new file mode 100644 index 0000000000..5c27be411f --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of switching the from-language to the same value + * that is currently selected in the to-language by opening the language dropdown menu, + * effectively stealing the to-language's value, leaving it unselected and focused. + */ +add_task( + async function test_select_translations_panel_select_same_from_language_via_popup() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSection: true, + openAtSpanishSection: true, + expectedFromLanguage: null, + expectedToLanguage: "en", + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["en"], { + openDropdownMenu: true, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewNoFromToSelected, + }); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of switching the to-language to the same value + * that is currently selected in the from-language by opening the language dropdown menu, + * effectively stealing the from-language's value, leaving it unselected and focused. + */ +add_task( + async function test_select_translations_panel_select_same_to_language_via_popup() { + const { cleanup, runInPage } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectEnglishSection: true, + openAtEnglishSection: true, + expectedFromLanguage: "en", + expectedToLanguage: null, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["en"], { + openDropdownMenu: true, + onChangeLanguage: + SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + }); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js new file mode 100644 index 0000000000..64d067d1f4 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of triggering a translation by directly switching + * the from-language to a valid selection when the panel is in the "idle" state without + * valid language pair. + */ +add_task( + async function test_select_translations_panel_translate_on_change_from_language_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSection: true, + openAtSpanishSection: true, + expectedFromLanguage: null, + expectedToLanguage: "en", + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["fr"], { + openDropdownMenu: false, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of triggering a translation by directly switching + * the to-language to a valid selection when the panel is in the "idle" state without + * valid language pair. + */ +add_task( + async function test_select_translations_panel_translate_on_change_to_language_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectEnglishSection: true, + openAtEnglishSection: true, + expectedFromLanguage: "en", + expectedToLanguage: null, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], { + openDropdownMenu: false, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js new file mode 100644 index 0000000000..0cd205d721 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of triggering a translation by switching the + * from-language to a valid selection by opening the language dropdown when the panel + * is in the "idle" state without valid language pair. + */ +add_task( + async function test_select_translations_panel_translate_on_change_from_language() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSection: true, + openAtSpanishSection: true, + expectedFromLanguage: null, + expectedToLanguage: "en", + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage(["fr"], { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of triggering a translation by switching the + * to-language to a valid selection by opening the language dropdown when the panel + * is in the "idle" state without valid language pair. + */ +add_task( + async function test_select_translations_panel_translate_on_change_to_language() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectEnglishSection: true, + openAtEnglishSection: true, + expectedFromLanguage: "en", + expectedToLanguage: null, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js new file mode 100644 index 0000000000..b3c02a96f6 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of directly changing the from-language in rapid succession, + * ensuring that any triggered translations are resolved/dropped in order, and that the final translated + * state matches the final selected language. + */ +add_task( + async function test_select_translations_panel_translate_on_change_from_language_multiple_times_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fa", toLang: "en" }, + { fromLang: "en", toLang: "fa" }, + { fromLang: "fi", toLang: "en" }, + { fromLang: "en", toLang: "fi" }, + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + { fromLang: "sl", toLang: "en" }, + { fromLang: "en", toLang: "sl" }, + { fromLang: "uk", toLang: "en" }, + { fromLang: "en", toLang: "uk" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + expectedFromLanguage: null, + expectedToLanguage: "en", + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage( + ["fa", "fi", "fr", "sl", "uk"], + { + openDropdownMenu: false, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + } + ); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of directly changing the to-language in rapid succession, + * ensuring that any triggered translations are resolved/dropped in order, and that the final translated + * state matches the final selected language. + */ +add_task( + async function test_select_translations_panel_translate_on_change_to_language_multiple_times_directly() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + { fromLang: "es", toLang: "en" }, + { fromLang: "en", toLang: "es" }, + { fromLang: "fa", toLang: "en" }, + { fromLang: "en", toLang: "fa" }, + { fromLang: "fi", toLang: "en" }, + { fromLang: "en", toLang: "fi" }, + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + { fromLang: "sl", toLang: "en" }, + { fromLang: "en", toLang: "sl" }, + { fromLang: "uk", toLang: "en" }, + { fromLang: "en", toLang: "uk" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + openAtEnglishHyperlink: true, + expectedFromLanguage: "en", + expectedToLanguage: null, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage( + ["es", "fa", "fi", "fr", "sl", "uk", "fa"], + { + openDropdownMenu: false, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + } + ); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js new file mode 100644 index 0000000000..50c877cfbc --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js @@ -0,0 +1,98 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case verifies the behavior of directly changing the from-language in rapid succession + * by opening the language dropdown menu, ensuring that any triggered translations are resolved/dropped + * in order, and that the final translated state matches the final selected language. + */ +add_task( + async function test_select_translations_panel_translate_on_change_from_language_multiple_times_via_popup() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + // Do not include Spanish. + { fromLang: "fa", toLang: "en" }, + { fromLang: "en", toLang: "fa" }, + { fromLang: "fi", toLang: "en" }, + { fromLang: "en", toLang: "fi" }, + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + { fromLang: "sl", toLang: "en" }, + { fromLang: "en", toLang: "sl" }, + { fromLang: "uk", toLang: "en" }, + { fromLang: "en", toLang: "uk" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectSpanishSentence: true, + openAtSpanishSentence: true, + expectedFromLanguage: null, + expectedToLanguage: "en", + onOpenPanel: + SelectTranslationsTestUtils.assertPanelViewNoFromLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedFromLanguage( + ["fa", "fi", "fr", "sl", "uk"], + { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + } + ); + + await cleanup(); + } +); + +/** + * This test case verifies the behavior of directly changing the to-language in rapid succession + * by opening the language dropdown menu, ensuring that any triggered translations are resolved/dropped + * in order, and that the final translated state matches the final selected language. + */ +add_task( + async function test_select_translations_panel_translate_on_change_to_language_multiple_times_via_popup() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: [ + { fromLang: "es", toLang: "en" }, + { fromLang: "en", toLang: "es" }, + { fromLang: "fa", toLang: "en" }, + { fromLang: "en", toLang: "fa" }, + { fromLang: "fi", toLang: "en" }, + { fromLang: "en", toLang: "fi" }, + { fromLang: "fr", toLang: "en" }, + { fromLang: "en", toLang: "fr" }, + { fromLang: "sl", toLang: "en" }, + { fromLang: "en", toLang: "sl" }, + { fromLang: "uk", toLang: "en" }, + { fromLang: "en", toLang: "uk" }, + ], + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectEnglishSentence: true, + openAtEnglishSentence: true, + expectedFromLanguage: "en", + expectedToLanguage: null, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewNoToLangSelected, + }); + + await SelectTranslationsTestUtils.changeSelectedToLanguage( + ["es", "fa", "fi", "fr", "sl", "uk"], + { + openDropdownMenu: true, + downloadHandler: resolveDownloads, + onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated, + } + ); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_open.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_open.js new file mode 100644 index 0000000000..7c7d6d88c9 --- /dev/null +++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_open.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test case tests the case of opening the SelectTranslationsPanel to a valid + * language pair from a short selection of text, which should trigger a translation + * on panel open. + */ +add_task( + async function test_select_translations_panel_translate_sentence_on_open() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSentence: true, + openAtFrenchSentence: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case tests the case of opening the SelectTranslationsPanel to a valid + * language pair from hyperlink text, which should trigger a translation on panel open. + */ +add_task( + async function test_select_translations_panel_translate_link_text_on_open() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + openAtSpanishHyperlink: true, + expectedFromLanguage: "es", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); + +/** + * This test case tests the case of opening the SelectTranslationsPanel to a valid + * language pair from a long selection of text, which should trigger a translation + * on panel open. + */ +add_task( + async function test_select_translations_panel_translate_long_text_on_open() { + const { cleanup, runInPage, resolveDownloads } = await loadTestPage({ + page: SELECT_TEST_PAGE_URL, + languagePairs: LANGUAGE_PAIRS, + prefs: [["browser.translations.select.enable", true]], + }); + + await SelectTranslationsTestUtils.openPanel(runInPage, { + selectFrenchSection: true, + openAtFrenchSection: true, + expectedFromLanguage: "fr", + expectedToLanguage: "en", + downloadHandler: resolveDownloads, + onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated, + }); + + await SelectTranslationsTestUtils.clickDoneButton(); + + await cleanup(); + } +); diff --git a/browser/components/translations/tests/browser/head.js b/browser/components/translations/tests/browser/head.js index 200ed08719..454de9146b 100644 --- a/browser/components/translations/tests/browser/head.js +++ b/browser/components/translations/tests/browser/head.js @@ -18,7 +18,7 @@ async function addTab(url) { const tab = await BrowserTestUtils.openNewForegroundTab( gBrowser, url, - true // Wait for laod + true // Wait for load ); return { tab, @@ -65,7 +65,6 @@ function click(element, message) { */ function getAllByL10nId(l10nId, doc = document) { const elements = doc.querySelectorAll(`[data-l10n-id="${l10nId}"]`); - console.log(doc); if (elements.length === 0) { throw new Error("Could not find the element by l10n id: " + l10nId); } @@ -285,6 +284,19 @@ async function toggleReaderMode() { */ class SharedTranslationsTestUtils { /** + * Asserts that the specified element currently has focus. + * + * @param {Element} element - The element to check for focus. + */ + static _assertHasFocus(element) { + is( + document.activeElement, + element, + `The element '${element.id}' should have focus.` + ); + } + + /** * Asserts that the mainViewId of the panel matches the given string. * * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel @@ -300,53 +312,30 @@ class SharedTranslationsTestUtils { } /** - * Asserts that the selected from-language matches the provided arguments. + * Asserts that the selected language in the menu matches the langTag or l10nId. * - * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel - * - The UI component or panel whose selected from-language is being asserted. - * @param {object} options - An object containing assertion parameters. - * @param {string} [options.langTag] - A BCP-47 language tag. - * @param {string} [options.l10nId] - A localization identifier. + * @param {Element} menuList - The menu list element to check. + * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against. + * @param {string} [options.langTag] - The BCP-47 language tag to match. + * @param {string} [options.l10nId] - The localization Id to match. */ - static _assertSelectedFromLanguage(panel, { langTag, l10nId }) { - const { fromMenuList } = panel.elements; - is( - fromMenuList.value, - langTag, - "Expected selected from-language to match the given language tag" + static _assertSelectedLanguage(menuList, { langTag, l10nId }) { + ok( + menuList.label, + `The label for the menulist ${menuList.id} should not be empty.` ); - if (l10nId) { - is( - fromMenuList.getAttribute("data-l10n-id"), - l10nId, - "Expected selected from-language to match the given l10n id" - ); - } - } - - /** - * Asserts that the selected to-language matches the provided arguments. - * - * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel - * - The UI component or panel whose selected from-language is being asserted. - * @param {object} options - An object containing assertion parameters. - * @param {string} [options.langTag] - A BCP-47 language tag. - * @param {string} [options.l10nId] - A localization identifier. - */ - static _assertSelectedToLanguage(panel, { langTag, l10nId }) { - const { toMenuList } = panel.elements; if (langTag) { is( - toMenuList.value, + menuList.value, langTag, - "Expected selected to-language to match the given language tag" + `Expected ${menuList.id} selection to match '${langTag}'` ); } if (l10nId) { is( - toMenuList.getAttribute("data-l10n-id"), + menuList.getAttribute("data-l10n-id"), l10nId, - "Expected selected to-language to match the given l10n id" + `Expected ${menuList.id} l10nId to match '${l10nId}'` ); } } @@ -391,6 +380,7 @@ class SharedTranslationsTestUtils { * This is often used to trigger the event on the expected element. * @param {Function|null} [postEventAssertion=null] - An optional callback function to execute after * the event has occurred. + * @param {ChromeWindow} [win] * @throws Throws if the element with the specified `elementId` does not exist. * @returns {Promise<void>} */ @@ -398,18 +388,21 @@ class SharedTranslationsTestUtils { elementId, eventName, callback, - postEventAssertion = null + postEventAssertion = null, + win = window ) { - const element = document.getElementById(elementId); + const element = win.document.getElementById(elementId); if (!element) { - throw new Error("Unable to find the translations panel element."); + throw new Error( + `Unable to find the ${elementId} element in the document.` + ); } const promise = BrowserTestUtils.waitForEvent(element, eventName); await callback(); - info("Waiting for the translations panel popup to be shown"); + info(`Waiting for the ${elementId} ${eventName} event`); await promise; if (postEventAssertion) { - postEventAssertion(); + await postEventAssertion(); } // Wait a single tick on the event loop. await new Promise(resolve => setTimeout(resolve, 0)); @@ -568,10 +561,12 @@ class FullPageTranslationsTestUtils { * * @param {string} fromLanguage - The BCP-47 language tag being translated from. * @param {string} toLanguage - The BCP-47 language tag being translated into. + * @param {ChromeWindow} win */ - static async #assertLangTagIsShownOnTranslationsButton( + static async assertLangTagIsShownOnTranslationsButton( fromLanguage, - toLanguage + toLanguage, + win = window ) { info( `Ensuring that the translations button displays the language tag "${toLanguage}"` @@ -579,7 +574,8 @@ class FullPageTranslationsTestUtils { const { button, locale } = await FullPageTranslationsTestUtils.assertTranslationsButton( { button: true, circleArrows: false, locale: true, icon: true }, - "The icon presents the locale." + "The icon presents the locale.", + win ); is( locale.innerText, @@ -605,12 +601,14 @@ class FullPageTranslationsTestUtils { * @param {string} toLanguage - The BCP-47 language tag being translated into. * @param {Function} runInPage - Allows running a closure in the content page. * @param {string} message - An optional message to log to info. + * @param {ChromeWindow} [win] */ static async assertPageIsTranslated( fromLanguage, toLanguage, runInPage, - message = null + message = null, + win = window ) { if (message) { info(message); @@ -625,9 +623,10 @@ class FullPageTranslationsTestUtils { ); }; await runInPage(callback, { fromLang: fromLanguage, toLang: toLanguage }); - await FullPageTranslationsTestUtils.#assertLangTagIsShownOnTranslationsButton( + await FullPageTranslationsTestUtils.assertLangTagIsShownOnTranslationsButton( fromLanguage, - toLanguage + toLanguage, + win ); } @@ -668,6 +667,9 @@ class FullPageTranslationsTestUtils { changeSourceLanguageButton: false, dismissErrorButton: false, error: false, + errorMessage: false, + errorMessageHint: false, + errorHintAction: false, fromMenuList: false, fromLabel: false, header: false, @@ -744,6 +746,34 @@ class FullPageTranslationsTestUtils { } /** + * Asserts that panel element visibility matches the initialization-failure view. + */ + static assertPanelViewInitFailure() { + info("Checking that the panel shows the default view"); + const { translateButton } = FullPageTranslationsPanel.elements; + FullPageTranslationsTestUtils.#assertPanelMainViewId( + "full-page-translations-panel-view-default" + ); + FullPageTranslationsTestUtils.#assertPanelElementVisibility({ + cancelButton: true, + error: true, + errorMessage: true, + errorMessageHint: true, + errorHintAction: true, + header: true, + translateButton: true, + }); + is( + translateButton.disabled, + true, + "The translate button should be disabled." + ); + FullPageTranslationsTestUtils.#assertPanelHeaderL10nId( + "translations-panel-header" + ); + } + + /** * Asserts that panel element visibility matches the panel error view. */ static assertPanelViewError() { @@ -753,6 +783,7 @@ class FullPageTranslationsTestUtils { ); FullPageTranslationsTestUtils.#assertPanelElementVisibility({ error: true, + errorMessage: true, ...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations, }); FullPageTranslationsTestUtils.#assertPanelHeaderL10nId( @@ -854,25 +885,31 @@ class FullPageTranslationsTestUtils { /** * Asserts that the selected from-language matches the provided language tag. * - * @param {string} langTag - A BCP-47 language tag. + * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against. + * @param {string} [options.langTag] - The BCP-47 language tag to match. + * @param {string} [options.l10nId] - The localization Id to match. */ static assertSelectedFromLanguage({ langTag, l10nId }) { - SharedTranslationsTestUtils._assertSelectedFromLanguage( - FullPageTranslationsPanel, - { langTag, l10nId } - ); + const { fromMenuList } = FullPageTranslationsPanel.elements; + SharedTranslationsTestUtils._assertSelectedLanguage(fromMenuList, { + langTag, + l10nId, + }); } /** * Asserts that the selected to-language matches the provided language tag. * - * @param {string} langTag - A BCP-47 language tag. + * @param {object} options - Options containing 'langTag' and 'l10nId' to assert against. + * @param {string} [options.langTag] - The BCP-47 language tag to match. + * @param {string} [options.l10nId] - The localization Id to match. */ static assertSelectedToLanguage({ langTag, l10nId }) { - SharedTranslationsTestUtils._assertSelectedToLanguage( - FullPageTranslationsPanel, - { langTag, l10nId } - ); + const { toMenuList } = FullPageTranslationsPanel.elements; + SharedTranslationsTestUtils._assertSelectedLanguage(toMenuList, { + langTag, + l10nId, + }); } /** @@ -880,16 +917,21 @@ class FullPageTranslationsTestUtils { * * @param {Record<string, boolean>} visibleAssertions * @param {string} message The message for the assertion. + * @param {ChromeWindow} [win] * @returns {HTMLElement} */ - static async assertTranslationsButton(visibleAssertions, message) { + static async assertTranslationsButton( + visibleAssertions, + message, + win = window + ) { const elements = { - button: document.getElementById("translations-button"), - icon: document.getElementById("translations-button-icon"), - circleArrows: document.getElementById( + button: win.document.getElementById("translations-button"), + icon: win.document.getElementById("translations-button-icon"), + circleArrows: win.document.getElementById( "translations-button-circle-arrows" ), - locale: document.getElementById("translations-button-locale"), + locale: win.document.getElementById("translations-button-locale"), }; for (const [name, element] of Object.entries(elements)) { @@ -1083,25 +1125,31 @@ class FullPageTranslationsTestUtils { * @param {boolean} config.pivotTranslation * - True if the expected translation is a pivot translation, otherwise false. * Affects the number of expected downloads. + * @param {ChromeWindow} [config.win] + * - An optional ChromeWindow, for multi-window tests. */ static async clickTranslateButton({ downloadHandler = null, pivotTranslation = false, + win = window, } = {}) { logAction(); - const { translateButton } = FullPageTranslationsPanel.elements; + const { translateButton } = win.FullPageTranslationsPanel.elements; assertVisibility({ visible: { translateButton } }); await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popuphidden", () => { click(translateButton); - } + }, + null /* postEventAssertion */, + win ); if (downloadHandler) { await FullPageTranslationsTestUtils.assertTranslationsButton( { button: true, circleArrows: true, locale: false, icon: true }, - "The icon presents the loading indicator." + "The icon presents the loading indicator.", + win ); await downloadHandler(pivotTranslation ? 2 : 1); } @@ -1117,21 +1165,26 @@ class FullPageTranslationsTestUtils { * - Open the panel from the app menu. If false, uses the translations button. * @param {boolean} config.openWithKeyboard * - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse. + * @param {ChromeWindow} [config.win] + * - An optional window for multi-window tests. */ static async openPanel({ onOpenPanel = null, openFromAppMenu = false, openWithKeyboard = false, + win = window, }) { logAction(); - await closeAllOpenPanelsAndMenus(); + await closeAllOpenPanelsAndMenus(win); if (openFromAppMenu) { await FullPageTranslationsTestUtils.#openPanelViaAppMenu({ + win, onOpenPanel, openWithKeyboard, }); } else { await FullPageTranslationsTestUtils.#openPanelViaTranslationsButton({ + win, onOpenPanel, openWithKeyboard, }); @@ -1146,21 +1199,26 @@ class FullPageTranslationsTestUtils { * - A function to run as soon as the panel opens. * @param {boolean} config.openWithKeyboard * - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse. + * @param {ChromeWindow} [config.win] */ static async #openPanelViaAppMenu({ onOpenPanel = null, openWithKeyboard = false, + win = window, }) { logAction(); - const appMenuButton = getById("PanelUI-menu-button"); + const appMenuButton = getById("PanelUI-menu-button", win.document); if (openWithKeyboard) { hitEnterKey(appMenuButton, "Opening the app-menu button with keyboard"); } else { click(appMenuButton, "Opening the app-menu button"); } - await BrowserTestUtils.waitForEvent(window.PanelUI.mainView, "ViewShown"); + await BrowserTestUtils.waitForEvent(win.PanelUI.mainView, "ViewShown"); - const translateSiteButton = getById("appMenu-translate-button"); + const translateSiteButton = getById( + "appMenu-translate-button", + win.document + ); is( translateSiteButton.disabled, @@ -1189,16 +1247,19 @@ class FullPageTranslationsTestUtils { * - A function to run as soon as the panel opens. * @param {boolean} config.openWithKeyboard * - Open the panel by synthesizing the keyboard. If false, synthesizes the mouse. + * @param {ChromeWindow} [config.win] */ static async #openPanelViaTranslationsButton({ onOpenPanel = null, openWithKeyboard = false, + win = window, }) { logAction(); const { button } = await FullPageTranslationsTestUtils.assertTranslationsButton( { button: true }, - "The translations button is visible." + "The translations button is visible.", + win ); await FullPageTranslationsTestUtils.waitForPanelPopupEvent( "popupshown", @@ -1209,7 +1270,8 @@ class FullPageTranslationsTestUtils { click(button, "Opening the popup"); } }, - onOpenPanel + onOpenPanel, + win ); } @@ -1242,7 +1304,7 @@ class FullPageTranslationsTestUtils { * * @param {string} langTag - A BCP-47 language tag. */ - static switchSelectedFromLanguage(langTag) { + static changeSelectedFromLanguage(langTag) { logAction(langTag); const { fromMenuList } = FullPageTranslationsPanel.elements; fromMenuList.value = langTag; @@ -1254,7 +1316,7 @@ class FullPageTranslationsTestUtils { * * @param {string} langTag - A BCP-47 language tag. */ - static switchSelectedToLanguage(langTag) { + static changeSelectedToLanguage(langTag) { logAction(langTag); const { toMenuList } = FullPageTranslationsPanel.elements; toMenuList.value = langTag; @@ -1270,20 +1332,23 @@ class FullPageTranslationsTestUtils { * @param {Function} callback * @param {Function} postEventAssertion * An optional assertion to be made immediately after the event occurs. + * @param {ChromeWindow} [win] * @returns {Promise<void>} */ static async waitForPanelPopupEvent( eventName, callback, - postEventAssertion = null + postEventAssertion = null, + win = window ) { // De-lazify the panel elements. - FullPageTranslationsPanel.elements; + win.FullPageTranslationsPanel.elements; await SharedTranslationsTestUtils._waitForPopupEvent( "full-page-translations-panel", eventName, callback, - postEventAssertion + postEventAssertion, + win ); } } @@ -1297,31 +1362,47 @@ class SelectTranslationsTestUtils { * * @param {Function} runInPage - A content-exposed function to run within the context of the page. * @param {object} options - Options for how to open the context menu and what properties to assert about the translate-selection item. - * @param {boolean} options.selectFirstParagraph - Selects the first paragraph before opening the context menu. - * @param {boolean} options.selectSpanishParagraph - Selects the Spanish paragraph before opening the context menu. - * This is only available in SPANISH_TEST_PAGE. - * @param {boolean} options.expectMenuItemIsVisible - Whether the translate-selection item is expected to be visible. - * Does not assert visibility if left undefined. - * @param {string} options.expectedTargetLanguage - The target language for translation. - * @param {boolean} options.openAtFirstParagraph - Opens the context menu at the first paragraph in the test page. - * @param {boolean} options.openAtSpanishParagraph - Opens the context menu at the Spanish paragraph in the test page. - * This is only available in SPANISH_TEST_PAGE. - * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at the English hyperlink in the test page. - * This is only available in SPANISH_TEST_PAGE. - * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at the Spanish hyperlink in the test page. - * This is only available in SPANISH_TEST_PAGE. + * + * The following options will only work when testing SELECT_TEST_PAGE_URL. + * + * @param {boolean} options.expectMenuItemVisible - Whether the select-translations menu item should be present in the context menu. + * @param {boolean} options.expectedTargetLanguage - The expected target language to be shown in the context menu. + * @param {boolean} options.selectFrenchSection - Selects the section of French text. + * @param {boolean} options.selectEnglishSection - Selects the section of English text. + * @param {boolean} options.selectSpanishSection - Selects the section of Spanish text. + * @param {boolean} options.selectFrenchSentence - Selects a French sentence. + * @param {boolean} options.selectEnglishSentence - Selects an English sentence. + * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence. + * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text. + * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text. + * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text. + * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence. + * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence. + * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence. + * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text. + * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at an hyperlinked English text. + * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text. * @param {string} [message] - A message to log to info. * @throws Throws an error if the properties of the translate-selection item do not match the expected options. */ static async assertContextMenuTranslateSelectionItem( runInPage, { - selectFirstParagraph, - selectSpanishParagraph, - expectMenuItemIsVisible, + expectMenuItemVisible, expectedTargetLanguage, - openAtFirstParagraph, - openAtSpanishParagraph, + selectFrenchSection, + selectEnglishSection, + selectSpanishSection, + selectFrenchSentence, + selectEnglishSentence, + selectSpanishSentence, + openAtFrenchSection, + openAtEnglishSection, + openAtSpanishSection, + openAtFrenchSentence, + openAtEnglishSentence, + openAtSpanishSentence, + openAtFrenchHyperlink, openAtEnglishHyperlink, openAtSpanishHyperlink, }, @@ -1336,10 +1417,21 @@ class SelectTranslationsTestUtils { await closeAllOpenPanelsAndMenus(); await SelectTranslationsTestUtils.openContextMenu(runInPage, { - selectFirstParagraph, - selectSpanishParagraph, - openAtFirstParagraph, - openAtSpanishParagraph, + expectMenuItemVisible, + expectedTargetLanguage, + selectFrenchSection, + selectEnglishSection, + selectSpanishSection, + selectFrenchSentence, + selectEnglishSentence, + selectSpanishSentence, + openAtFrenchSection, + openAtEnglishSection, + openAtSpanishSection, + openAtFrenchSentence, + openAtEnglishSentence, + openAtSpanishSentence, + openAtFrenchHyperlink, openAtEnglishHyperlink, openAtSpanishHyperlink, }); @@ -1349,16 +1441,21 @@ class SelectTranslationsTestUtils { /* ensureIsVisible */ false ); - if (expectMenuItemIsVisible !== undefined) { - const visibility = expectMenuItemIsVisible ? "visible" : "hidden"; - assertVisibility({ [visibility]: menuItem }); + if (expectMenuItemVisible !== undefined) { + const visibility = expectMenuItemVisible ? "visible" : "hidden"; + assertVisibility({ [visibility]: { menuItem } }); } - if (expectMenuItemIsVisible === true) { + if (expectMenuItemVisible === true) { if (expectedTargetLanguage) { // Target language expected, check for the data-l10n-id with a `{$language}` argument. const expectedL10nId = - selectFirstParagraph === true || selectSpanishParagraph === true + selectFrenchSection || + selectEnglishSection || + selectSpanishSection || + selectFrenchSentence || + selectEnglishSentence || + selectSpanishSentence ? "main-context-menu-translate-selection-to-language" : "main-context-menu-translate-link-text-to-language"; await waitForCondition( @@ -1381,7 +1478,12 @@ class SelectTranslationsTestUtils { } else { // No target language expected, check for the data-l10n-id that has no `{$language}` argument. const expectedL10nId = - selectFirstParagraph === true || selectSpanishParagraph === true + selectFrenchSection || + selectEnglishSection || + selectSpanishSection || + selectFrenchSentence || + selectEnglishSentence || + selectSpanishSentence ? "main-context-menu-translate-selection" : "main-context-menu-translate-link-text"; await waitForCondition( @@ -1399,6 +1501,21 @@ class SelectTranslationsTestUtils { } /** + * Elements that should always be visible in the SelectTranslationsPanel. + */ + static #alwaysPresentElements = { + betaIcon: true, + copyButton: true, + doneButton: true, + fromLabel: true, + fromMenuList: true, + header: true, + toLabel: true, + toMenuList: true, + textArea: true, + }; + + /** * Asserts that for each provided expectation, the visible state of the corresponding * element in FullPageTranslationsPanel.elements both exists and matches the visibility expectation. * @@ -1409,16 +1526,7 @@ class SelectTranslationsTestUtils { SharedTranslationsTestUtils._assertPanelElementVisibility( SelectTranslationsPanel.elements, { - betaIcon: false, - copyButton: false, - doneButton: false, - fromLabel: false, - fromMenuList: false, - header: false, - textArea: false, - toLabel: false, - toMenuList: false, - translateFullPageButton: false, + ...SelectTranslationsTestUtils.#alwaysPresentElements, // Overwrite any of the above defaults with the passed in expectations. ...expectations, } @@ -1426,49 +1534,255 @@ class SelectTranslationsTestUtils { } /** - * Asserts that the mainViewId of the panel matches the given string. - * - * @param {string} expectedId + * Asserts that the SelectTranslationsPanel UI matches the expected + * state when the panel has completed its translation. */ - static #assertPanelMainViewId(expectedId) { - SharedTranslationsTestUtils._assertPanelMainViewId( - SelectTranslationsPanel, - expectedId + static assertPanelViewTranslated() { + const { textArea } = SelectTranslationsPanel.elements; + ok( + !textArea.classList.contains("translating"), + "The textarea should not have the translating class." + ); + SelectTranslationsTestUtils.#assertPanelElementVisibility({ + ...SelectTranslationsTestUtils.#alwaysPresentElements, + }); + SelectTranslationsTestUtils.#assertConditionalUIEnabled({ + textArea: true, + copyButton: true, + translateFullPageButton: true, + }); + SelectTranslationsTestUtils.#assertPanelHasTranslatedText(); + SelectTranslationsTestUtils.#assertPanelTextAreaHeight(); + SelectTranslationsTestUtils.#assertPanelTextAreaOverflow(); + } + + static #assertPanelTextAreaDirection(langTag = null) { + const expectedTextDirection = langTag + ? Services.intl.getScriptDirection(langTag) + : null; + const { textArea } = SelectTranslationsPanel.elements; + const actualTextDirection = textArea.getAttribute("dir"); + + is( + actualTextDirection, + expectedTextDirection, + `The text direction should be ${expectedTextDirection}` ); } /** - * Asserts that panel element visibility matches the default panel view. + * Asserts that the SelectTranslationsPanel translated text area is + * both scrollable and scrolled to the top. */ - static assertPanelViewDefault() { - info("Checking that the select-translations panel shows the default view"); - SelectTranslationsTestUtils.#assertPanelMainViewId( - "select-translations-panel-view-default" + static #assertPanelTextAreaOverflow() { + const { textArea } = SelectTranslationsPanel.elements; + is( + textArea.style.overflow, + "auto", + "The translated-text area should be scrollable." + ); + if (textArea.scrollHeight > textArea.clientHeight) { + is( + textArea.scrollTop, + 0, + "The translated-text area should be scrolled to the top." + ); + } + } + + /** + * Asserts that the SelectTranslationsPanel translated text area is + * the correct height for the length of the translated text. + */ + static #assertPanelTextAreaHeight() { + const { textArea } = SelectTranslationsPanel.elements; + + if ( + SelectTranslationsPanel.getSourceText().length < + SelectTranslationsPanel.textLengthThreshold + ) { + is( + textArea.style.height, + SelectTranslationsPanel.shortTextHeight, + "The panel text area should have the short-text height" + ); + } else { + is( + textArea.style.height, + SelectTranslationsPanel.longTextHeight, + "The panel text area should have the long-text height" + ); + } + } + + /** + * Asserts that the SelectTranslationsPanel UI matches the expected + * state when the panel is actively translating text. + */ + static assertPanelViewActivelyTranslating() { + const { textArea } = SelectTranslationsPanel.elements; + ok( + textArea.classList.contains("translating"), + "The textarea should have the translating class." ); SelectTranslationsTestUtils.#assertPanelElementVisibility({ - betaIcon: true, - fromLabel: true, - fromMenuList: true, - header: true, + ...SelectTranslationsTestUtils.#alwaysPresentElements, + }); + SelectTranslationsTestUtils.#assertPanelHasTranslatingPlaceholder(); + } + + /** + * Asserts that the SelectTranslationsPanel UI matches the expected + * state when no from-language is selected in the panel. + */ + static async assertPanelViewNoFromLangSelected() { + const { textArea } = SelectTranslationsPanel.elements; + ok( + !textArea.classList.contains("translating"), + "The textarea should not have the translating class." + ); + SelectTranslationsTestUtils.#assertPanelElementVisibility({ + ...SelectTranslationsTestUtils.#alwaysPresentElements, + }); + await SelectTranslationsTestUtils.#assertPanelHasIdlePlaceholder(); + SelectTranslationsTestUtils.#assertConditionalUIEnabled({ + textArea: false, + copyButton: false, + translateFullPageButton: false, + }); + SelectTranslationsTestUtils.assertSelectedFromLanguage(null); + } + + /** + * Asserts that the SelectTranslationsPanel UI matches the expected + * state when no to-language is selected in the panel. + */ + static async assertPanelViewNoToLangSelected() { + const { textArea } = SelectTranslationsPanel.elements; + ok( + !textArea.classList.contains("translating"), + "The textarea should not have the translating class." + ); + SelectTranslationsTestUtils.#assertPanelElementVisibility({ + ...SelectTranslationsTestUtils.#alwaysPresentElements, + }); + SelectTranslationsTestUtils.assertSelectedToLanguage(null); + SelectTranslationsTestUtils.#assertConditionalUIEnabled({ + textArea: false, + copyButton: false, + translateFullPageButton: false, + }); + await SelectTranslationsTestUtils.#assertPanelHasIdlePlaceholder(); + } + + /** + * Asserts that the SelectTranslationsPanel UI contains the + * idle placeholder text. + */ + static async #assertPanelHasIdlePlaceholder() { + const { textArea } = SelectTranslationsPanel.elements; + const expected = await document.l10n.formatValue( + "select-translations-panel-idle-placeholder-text" + ); + is( + textArea.value, + expected, + "Translated text area should be the idle placeholder." + ); + SelectTranslationsTestUtils.#assertPanelTextAreaDirection(); + } + + /** + * Asserts that the SelectTranslationsPanel UI contains the + * translating placeholder text. + */ + static async #assertPanelHasTranslatingPlaceholder() { + const { textArea } = SelectTranslationsPanel.elements; + const expected = await document.l10n.formatValue( + "select-translations-panel-translating-placeholder-text" + ); + is( + textArea.value, + expected, + "Active translation text area should have the translating placeholder." + ); + SelectTranslationsTestUtils.#assertPanelTextAreaDirection(); + SelectTranslationsTestUtils.#assertConditionalUIEnabled({ + textArea: true, + copyButton: false, + translateFullPageButton: true, + }); + } + + /** + * Asserts that the SelectTranslationsPanel UI contains the + * translated text. + */ + static #assertPanelHasTranslatedText() { + const { textArea, fromMenuList, toMenuList } = + SelectTranslationsPanel.elements; + const fromLanguage = fromMenuList.value; + const toLanguage = toMenuList.value; + const translatedSuffix = ` [${fromLanguage} to ${toLanguage}]`; + ok( + textArea.value.endsWith(translatedSuffix), + `Translated text should match ${fromLanguage} to ${toLanguage}` + ); + is( + SelectTranslationsPanel.getSourceText().length, + SelectTranslationsPanel.getTranslatedText().length - + translatedSuffix.length, + "Expected translated text length to correspond to the source text length." + ); + SelectTranslationsTestUtils.#assertPanelTextAreaDirection(toLanguage); + SelectTranslationsTestUtils.#assertConditionalUIEnabled({ textArea: true, - toLabel: true, - toMenuList: true, copyButton: true, - doneButton: true, translateFullPageButton: true, }); } /** + * Asserts the enabled state of action buttons in the SelectTranslationsPanel. + * + * @param {boolean} expectEnabled - Whether the buttons should be enabled (true) or not (false). + */ + static #assertConditionalUIEnabled({ + copyButton: copyButtonEnabled, + translateFullPageButton: translateFullPageButtonEnabled, + textArea: textAreaEnabled, + }) { + const { copyButton, translateFullPageButton, textArea } = + SelectTranslationsPanel.elements; + is( + copyButton.disabled, + !copyButtonEnabled, + `The copy button should be ${copyButtonEnabled ? "enabled" : "disabled"}.` + ); + is( + translateFullPageButton.disabled, + !translateFullPageButtonEnabled, + `The translate-full-page button should be ${ + translateFullPageButtonEnabled ? "enabled" : "disabled" + }.` + ); + is( + textArea.disabled, + !textAreaEnabled, + `The translated-text area should be ${ + textAreaEnabled ? "enabled" : "disabled" + }.` + ); + } + + /** * Asserts that the selected from-language matches the provided language tag. * * @param {string} langTag - A BCP-47 language tag. */ - static assertSelectedFromLanguage({ langTag, l10nId }) { - SharedTranslationsTestUtils._assertSelectedFromLanguage( - SelectTranslationsPanel, - { langTag, l10nId } - ); + static assertSelectedFromLanguage(langTag = null) { + const { fromMenuList } = SelectTranslationsPanel.elements; + SelectTranslationsTestUtils.#assertSelectedLanguage(fromMenuList, langTag); } /** @@ -1476,11 +1790,29 @@ class SelectTranslationsTestUtils { * * @param {string} langTag - A BCP-47 language tag. */ - static assertSelectedToLanguage({ langTag, l10nId }) { - SharedTranslationsTestUtils._assertSelectedToLanguage( - SelectTranslationsPanel, - { langTag, l10nId } - ); + static assertSelectedToLanguage(langTag = null) { + const { toMenuList } = SelectTranslationsPanel.elements; + SelectTranslationsTestUtils.#assertSelectedLanguage(toMenuList, langTag); + } + + /** + * Asserts the selected language in the given menu list if a langTag is provided. + * If no langTag is given, asserts that the menulist displays the localized placeholder. + * + * @param {object} menuList - The menu list object to check. + * @param {string} [langTag] - The optional language tag to assert against. + */ + static #assertSelectedLanguage(menuList, langTag) { + if (langTag) { + SharedTranslationsTestUtils._assertSelectedLanguage(menuList, { + langTag, + }); + } else { + SharedTranslationsTestUtils._assertSelectedLanguage(menuList, { + l10nId: "translations-panel-choose-language", + }); + SharedTranslationsTestUtils._assertHasFocus(menuList); + } } /** @@ -1503,110 +1835,258 @@ class SelectTranslationsTestUtils { * * @param {Function} runInPage - A content-exposed function to run within the context of the page. * @param {object} options - Options for opening the context menu. - * @param {boolean} options.selectFirstParagraph - Selects the first paragraph before opening the context menu. - * @param {boolean} options.selectSpanishParagraph - Selects the Spanish paragraph before opening the context menu. - * This is only available in SPANISH_TEST_PAGE. - * @param {boolean} options.openAtFirstParagraph - Opens the context menu at the first paragraph in the test page. - * @param {boolean} options.openAtSpanishParagraph - Opens the context menu at the Spanish paragraph in the test page. - * This is only available in SPANISH_TEST_PAGE. - * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at the English hyperlink in the test page. - * This is only available in SPANISH_TEST_PAGE. - * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at the Spanish hyperlink in the test page. - * This is only available in SPANISH_TEST_PAGE. + * + * The following options will only work when testing SELECT_TEST_PAGE_URL. + * + * @param {boolean} options.selectFrenchSection - Selects the section of French text. + * @param {boolean} options.selectEnglishSection - Selects the section of English text. + * @param {boolean} options.selectSpanishSection - Selects the section of Spanish text. + * @param {boolean} options.selectFrenchSentence - Selects a French sentence. + * @param {boolean} options.selectEnglishSentence - Selects an English sentence. + * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence. + * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text. + * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text. + * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text. + * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence. + * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence. + * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence. + * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text. + * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at an hyperlinked English text. + * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text. * @throws Throws an error if no valid option was provided for opening the menu. */ - static async openContextMenu( - runInPage, - { - selectFirstParagraph, - selectSpanishParagraph, - openAtFirstParagraph, - openAtSpanishParagraph, - openAtEnglishHyperlink, - openAtSpanishHyperlink, - } - ) { + static async openContextMenu(runInPage, options) { logAction(); - if (selectFirstParagraph === true) { - await runInPage(async TranslationsTest => { - const { getFirstParagraph } = TranslationsTest.getSelectors(); - const paragraph = getFirstParagraph(); - TranslationsTest.selectContentElement(paragraph); - }); - } + const maybeSelectContentFrom = async keyword => { + const conditionVariableName = `select${keyword}`; + const selectorFunctionName = `get${keyword}`; + + if (options[conditionVariableName]) { + await runInPage( + async (TranslationsTest, data) => { + const selectorFunction = + TranslationsTest.getSelectors()[data.selectorFunctionName]; + if (typeof selectorFunction === "function") { + const paragraph = selectorFunction(); + TranslationsTest.selectContentElement(paragraph); + } + }, + { selectorFunctionName } + ); + } + }; - if (selectSpanishParagraph === true) { - await runInPage(async TranslationsTest => { - const { getSpanishParagraph } = TranslationsTest.getSelectors(); - const paragraph = getSpanishParagraph(); - TranslationsTest.selectContentElement(paragraph); - }); + await maybeSelectContentFrom("FrenchSection"); + await maybeSelectContentFrom("EnglishSection"); + await maybeSelectContentFrom("SpanishSection"); + await maybeSelectContentFrom("FrenchSentence"); + await maybeSelectContentFrom("EnglishSentence"); + await maybeSelectContentFrom("SpanishSentence"); + + const maybeOpenContextMenuAt = async keyword => { + const optionVariableName = `openAt${keyword}`; + const selectorFunctionName = `get${keyword}`; + + if (options[optionVariableName]) { + await SharedTranslationsTestUtils._waitForPopupEvent( + "contentAreaContextMenu", + "popupshown", + async () => { + await runInPage( + async (TranslationsTest, data) => { + const selectorFunction = + TranslationsTest.getSelectors()[data.selectorFunctionName]; + if (typeof selectorFunction === "function") { + const element = selectorFunction(); + await TranslationsTest.rightClickContentElement(element); + } + }, + { selectorFunctionName } + ); + } + ); + } + }; + + await maybeOpenContextMenuAt("FrenchSection"); + await maybeOpenContextMenuAt("EnglishSection"); + await maybeOpenContextMenuAt("SpanishSection"); + await maybeOpenContextMenuAt("FrenchSentence"); + await maybeOpenContextMenuAt("EnglishSentence"); + await maybeOpenContextMenuAt("SpanishSentence"); + await maybeOpenContextMenuAt("FrenchHyperlink"); + await maybeOpenContextMenuAt("EnglishHyperlink"); + await maybeOpenContextMenuAt("SpanishHyperlink"); + } + + /** + * Handles language-model downloads for the SelectTranslationsPanel, ensuring that expected + * UI states match based on the resolved download state. + * + * @param {object} options - Configuration options for downloads. + * @param {function(number): Promise<void>} options.downloadHandler - The function to resolve or reject the downloads. + * @param {boolean} [options.pivotTranslation] - Whether to expect a pivot translation. + * + * @returns {Promise<void>} + */ + static async handleDownloads({ downloadHandler, pivotTranslation }) { + const { textArea } = SelectTranslationsPanel.elements; + + if (downloadHandler) { + if (textArea.style.overflow !== "hidden") { + await BrowserTestUtils.waitForMutationCondition( + textArea, + { attributes: true, attributeFilter: ["style"] }, + () => textArea.style.overflow === "hidden" + ); + } + + await SelectTranslationsTestUtils.assertPanelViewActivelyTranslating(); + await downloadHandler(pivotTranslation ? 2 : 1); } - if (openAtFirstParagraph === true) { - await SharedTranslationsTestUtils._waitForPopupEvent( - "contentAreaContextMenu", - "popupshown", - async () => { - await runInPage(async TranslationsTest => { - const { getFirstParagraph } = TranslationsTest.getSelectors(); - const paragraph = getFirstParagraph(); - await TranslationsTest.rightClickContentElement(paragraph); - }); - } + if (textArea.style.overflow === "hidden") { + await BrowserTestUtils.waitForMutationCondition( + textArea, + { attributes: true, attributeFilter: ["style"] }, + () => textArea.style.overflow === "auto" ); - return; } + } - if (openAtSpanishParagraph === true) { - await SharedTranslationsTestUtils._waitForPopupEvent( - "contentAreaContextMenu", - "popupshown", - async () => { - await runInPage(async TranslationsTest => { - const { getSpanishParagraph } = TranslationsTest.getSelectors(); - const paragraph = getSpanishParagraph(); - await TranslationsTest.rightClickContentElement(paragraph); - }); - } + /** + * Switches the selected from-language to the provided language tags + * + * @param {string[]} langTags - An array of BCP-47 language tags. + * @param {object} options - Configuration options for the language change. + * @param {boolean} options.openDropdownMenu - Determines whether the language change should be made via a dropdown menu or directly. + * + * @returns {Promise<void>} + */ + static async changeSelectedFromLanguage(langTags, options) { + const { fromMenuList, fromMenuPopup } = SelectTranslationsPanel.elements; + const { openDropdownMenu } = options; + + const switchFn = openDropdownMenu + ? SelectTranslationsTestUtils.#changeSelectedLanguageViaDropdownMenu + : SelectTranslationsTestUtils.#changeSelectedLanguageDirectly; + + await switchFn( + langTags, + { menuList: fromMenuList, menuPopup: fromMenuPopup }, + options + ); + } + + /** + * Switches the selected to-language to the provided language tag. + * + * @param {string[]} langTags - An array of BCP-47 language tags. + * @param {object} options - Options for selecting paragraphs and opening the context menu. + * @param {boolean} options.openDropdownMenu - Determines whether the language change should be made via a dropdown menu or directly. + * @param {Function} options.downloadHandler - Handler for initiating downloads post language change, if applicable. + * @param {Function} options.onChangeLanguage - Callback function to be executed after the language change. + * + * @returns {Promise<void>} + */ + static async changeSelectedToLanguage(langTags, options) { + const { toMenuList, toMenuPopup } = SelectTranslationsPanel.elements; + const { openDropdownMenu } = options; + + const switchFn = openDropdownMenu + ? SelectTranslationsTestUtils.#changeSelectedLanguageViaDropdownMenu + : SelectTranslationsTestUtils.#changeSelectedLanguageDirectly; + + await switchFn( + langTags, + { menuList: toMenuList, menuPopup: toMenuPopup }, + options + ); + } + + /** + * Directly changes the selected language to each provided language tag without using a dropdown menu. + * + * @param {string[]} langTags - An array of BCP-47 language tags for direct selection. + * @param {object} elements - Elements required for changing the selected language. + * @param {Element} elements.menuList - The menu list element where languages are directly changed. + * @param {object} options - Configuration options for language change and additional actions. + * @param {Function} options.downloadHandler - Handler for initiating downloads post language change, if applicable. + * @param {Function} options.onChangeLanguage - Callback function to be executed after the language change. + * + * @returns {Promise<void>} + */ + static async #changeSelectedLanguageDirectly(langTags, elements, options) { + const { menuList } = elements; + const { onChangeLanguage, downloadHandler } = options; + + for (const langTag of langTags) { + const menuListUpdated = BrowserTestUtils.waitForMutationCondition( + menuList, + { attributes: true, attributeFilter: ["value"] }, + () => menuList.value === langTag ); - return; + + menuList.value = langTag; + menuList.dispatchEvent(new Event("command")); + await menuListUpdated; } - if (openAtEnglishHyperlink === true) { - await SharedTranslationsTestUtils._waitForPopupEvent( - "contentAreaContextMenu", - "popupshown", - async () => { - await runInPage(async TranslationsTest => { - const { getEnglishHyperlink } = TranslationsTest.getSelectors(); - const hyperlink = getEnglishHyperlink(); - await TranslationsTest.rightClickContentElement(hyperlink); - }); - } - ); - return; + if (downloadHandler) { + menuList.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + await SelectTranslationsTestUtils.handleDownloads(options); } - if (openAtSpanishHyperlink === true) { - await SharedTranslationsTestUtils._waitForPopupEvent( - "contentAreaContextMenu", + if (onChangeLanguage) { + await onChangeLanguage(); + } + } + + /** + * Changes the selected language by opening the dropdown menu for each provided language tag. + * + * @param {string[]} langTags - An array of BCP-47 language tags for selection via dropdown. + * @param {object} elements - Elements involved in the dropdown language selection process. + * @param {Element} elements.menuList - The element that triggers the dropdown menu. + * @param {Element} elements.menuPopup - The dropdown menu element containing selectable languages. + * @param {object} options - Configuration options for language change and additional actions. + * @param {Function} options.downloadHandler - Handler for initiating downloads post language change, if applicable. + * @param {Function} options.onChangeLanguage - Callback function to be executed after the language change. + * + * @returns {Promise<void>} + */ + static async #changeSelectedLanguageViaDropdownMenu( + langTags, + elements, + options + ) { + const { menuList, menuPopup } = elements; + const { onChangeLanguage } = options; + for (const langTag of langTags) { + await SelectTranslationsTestUtils.waitForPanelPopupEvent( "popupshown", - async () => { - await runInPage(async TranslationsTest => { - const { getSpanishHyperlink } = TranslationsTest.getSelectors(); - const hyperlink = getSpanishHyperlink(); - await TranslationsTest.rightClickContentElement(hyperlink); - }); + () => click(menuList) + ); + + const menuItem = menuPopup.querySelector(`[value="${langTag}"]`); + await SelectTranslationsTestUtils.waitForPanelPopupEvent( + "popuphidden", + () => { + click(menuItem); + // Synthesizing a click on the menuitem isn't closing the popup + // as a click normally would, so this tab keypress is added to + // ensure the popup closes. + EventUtils.synthesizeKey("KEY_Tab"); } ); - return; - } - throw new Error( - "openContextMenu() was not provided a declaration for which element to open the menu at." - ); + await SelectTranslationsTestUtils.handleDownloads(options); + if (onChangeLanguage) { + await onChangeLanguage(); + } + } } /** @@ -1614,36 +2094,32 @@ class SelectTranslationsTestUtils { * * @param {Function} runInPage - A content-exposed function to run within the context of the page. * @param {object} options - Options for selecting paragraphs and opening the context menu. - * @param {boolean} options.selectFirstParagraph - Selects the first paragraph before opening the context menu. - * @param {boolean} options.selectSpanishParagraph - Selects the Spanish paragraph before opening the context menu. - * This is only available in SPANISH_TEST_PAGE. - * @param {string} options.expectedTargetLanguage - The target language for translation. - * @param {boolean} options.openAtFirstParagraph - Opens the context menu at the first paragraph. - * @param {boolean} options.openAtSpanishParagraph - Opens at the Spanish paragraph. - * This is only available in SPANISH_TEST_PAGE. - * @param {boolean} options.openAtEnglishHyperlink - Opens at the English hyperlink. - * This is only available in SPANISH_TEST_PAGE. - * @param {boolean} options.openAtSpanishHyperlink - Opens at the Spanish hyperlink. - * This is only available in SPANISH_TEST_PAGE. - * @param {Function|null} [options.onOpenPanel=null] - An optional callback function to execute after the panel opens. - * @param {string|null} [message=null] - An optional message to log to info. + * + * The following options will only work when testing SELECT_TEST_PAGE_URL. + * + * @param {string} options.expectedFromLanguage - The expected from-language tag. + * @param {string} options.expectedToLanguage - The expected to-language tag. + * @param {boolean} options.selectFrenchSection - Selects the section of French text. + * @param {boolean} options.selectEnglishSection - Selects the section of English text. + * @param {boolean} options.selectSpanishSection - Selects the section of Spanish text. + * @param {boolean} options.selectFrenchSentence - Selects a French sentence. + * @param {boolean} options.selectEnglishSentence - Selects an English sentence. + * @param {boolean} options.selectSpanishSentence - Selects a Spanish sentence. + * @param {boolean} options.openAtFrenchSection - Opens the context menu at the section of French text. + * @param {boolean} options.openAtEnglishSection - Opens the context menu at the section of English text. + * @param {boolean} options.openAtSpanishSection - Opens the context menu at the section of Spanish text. + * @param {boolean} options.openAtFrenchSentence - Opens the context menu at a French sentence. + * @param {boolean} options.openAtEnglishSentence - Opens the context menu at an English sentence. + * @param {boolean} options.openAtSpanishSentence - Opens the context menu at a Spanish sentence. + * @param {boolean} options.openAtFrenchHyperlink - Opens the context menu at a hyperlinked French text. + * @param {boolean} options.openAtEnglishHyperlink - Opens the context menu at an hyperlinked English text. + * @param {boolean} options.openAtSpanishHyperlink - Opens the context menu at a hyperlinked Spanish text. + * @param {Function} [options.onOpenPanel] - An optional callback function to execute after the panel opens. + * @param {string|null} [message] - An optional message to log to info. * @throws Throws an error if the context menu could not be opened with the provided options. * @returns {Promise<void>} */ - static async openPanel( - runInPage, - { - selectFirstParagraph, - selectSpanishParagraph, - expectedTargetLanguage, - openAtFirstParagraph, - openAtSpanishParagraph, - openAtEnglishHyperlink, - openAtSpanishHyperlink, - onOpenPanel, - }, - message - ) { + static async openPanel(runInPage, options, message) { logAction(); if (message) { @@ -1652,15 +2128,7 @@ class SelectTranslationsTestUtils { await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem( runInPage, - { - selectFirstParagraph, - selectSpanishParagraph, - expectedTargetLanguage, - openAtFirstParagraph, - openAtSpanishParagraph, - openAtEnglishHyperlink, - openAtSpanishHyperlink, - }, + options, message ); @@ -1668,9 +2136,28 @@ class SelectTranslationsTestUtils { await SelectTranslationsTestUtils.waitForPanelPopupEvent( "popupshown", - () => click(menuItem), - onOpenPanel + async () => { + click(menuItem); + await closeContextMenuIfOpen(); + }, + async () => { + const { onOpenPanel } = options; + await SelectTranslationsTestUtils.handleDownloads(options); + if (onOpenPanel) { + await onOpenPanel(); + } + } ); + + const { expectedFromLanguage, expectedToLanguage } = options; + if (expectedFromLanguage !== undefined) { + SelectTranslationsTestUtils.assertSelectedFromLanguage( + expectedFromLanguage + ); + } + if (expectedToLanguage !== undefined) { + SelectTranslationsTestUtils.assertSelectedToLanguage(expectedToLanguage); + } } /** @@ -1732,10 +2219,10 @@ class TranslationsSettingsTestUtils { translateNeverHeader: document.getElementById( "translations-settings-never-translate" ), - translateAlwaysAddButton: document.getElementById( + translateAlwaysMenuList: document.getElementById( "translations-settings-always-translate-list" ), - translateNeverAddButton: document.getElementById( + translateNeverMenuList: document.getElementById( "translations-settings-never-translate-list" ), translateNeverSiteHeader: document.getElementById( @@ -1744,12 +2231,15 @@ class TranslationsSettingsTestUtils { translateNeverSiteDesc: document.getElementById( "translations-settings-never-sites" ), - translateDownloadLanguagesHeader: document.getElementById( - "translations-settings-download-languages" - ), + translateDownloadLanguagesHeader: document + .getElementById("translations-settings-download-section") + .querySelector("h2"), translateDownloadLanguagesLearnMore: document.getElementById( "download-languages-learn-more" ), + translateDownloadLanguagesList: document.getElementById( + "translations-settings-download-section" + ), }; return elements; diff --git a/browser/components/uitour/UITour-lib.js b/browser/components/uitour/UITour-lib.js index 0df3059425..a83ec95200 100644 --- a/browser/components/uitour/UITour-lib.js +++ b/browser/components/uitour/UITour-lib.js @@ -9,7 +9,7 @@ if (typeof Mozilla == "undefined") { var Mozilla = {}; } -(function ($) { +(function () { "use strict"; // create namespace diff --git a/browser/components/uitour/UITour.sys.mjs b/browser/components/uitour/UITour.sys.mjs index fef68a5a95..920815fec5 100644 --- a/browser/components/uitour/UITour.sys.mjs +++ b/browser/components/uitour/UITour.sys.mjs @@ -339,7 +339,7 @@ export var UITour = { let callback = buttonData.callbackID; let button = { label: buttonData.label, - callback: event => { + callback: () => { this.sendPageCallback(browser, callback); }, }; @@ -694,7 +694,7 @@ export var UITour = { } }, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { lazy.log.debug("observe: aTopic =", aTopic); switch (aTopic) { // The browser message manager is disconnected when the <browser> is @@ -918,7 +918,7 @@ export var UITour = { ); }, - getTarget(aWindow, aTargetName, aSticky = false) { + getTarget(aWindow, aTargetName) { lazy.log.debug("getTarget:", aTargetName); if (typeof aTargetName != "string" || !aTargetName) { lazy.log.warn("getTarget: Invalid target name specified"); @@ -1280,7 +1280,7 @@ export var UITour = { tooltipButtons.hidden = !aButtons.length; let tooltipClose = document.getElementById("UITourTooltipClose"); - let closeButtonCallback = event => { + let closeButtonCallback = () => { this.hideInfo(document.defaultView); if (aOptions && aOptions.closeButtonCallback) { aOptions.closeButtonCallback(); @@ -1301,7 +1301,7 @@ export var UITour = { tooltip.addEventListener( "popuphiding", - function (event) { + function () { tooltipClose.removeEventListener("command", closeButtonCallback); if (aOptions.targetCallback && aAnchor.removeTargetListener) { aAnchor.removeTargetListener(document, targetCallback); diff --git a/browser/components/uitour/test/browser_UITour.js b/browser/components/uitour/test/browser_UITour.js index d9e517af1f..c961ab3c0d 100644 --- a/browser/components/uitour/test/browser_UITour.js +++ b/browser/components/uitour/test/browser_UITour.js @@ -323,7 +323,7 @@ var tests = [ () => { highlight.addEventListener( "animationstart", - function (aEvent) { + function () { ok( true, "Animation occurred again even though the effect was the same" diff --git a/browser/components/uitour/test/browser_UITour3.js b/browser/components/uitour/test/browser_UITour3.js index 526994f420..2820fbd020 100644 --- a/browser/components/uitour/test/browser_UITour3.js +++ b/browser/components/uitour/test/browser_UITour3.js @@ -81,7 +81,7 @@ add_UITour_task(async function test_info_buttons_1() { ); is( buttons.children[0].getAttribute("image"), - "", + null, "Text should have no image" ); is(buttons.children[0].className, "", "Text should have no class"); @@ -94,7 +94,7 @@ add_UITour_task(async function test_info_buttons_1() { ); is( buttons.children[1].getAttribute("image"), - "", + null, "Link should have no image" ); is(buttons.children[1].className, "button-link", "Check link class"); @@ -107,7 +107,7 @@ add_UITour_task(async function test_info_buttons_1() { ); is( buttons.children[2].getAttribute("image"), - "", + null, "First button should have no image" ); is(buttons.children[2].className, "", "Button 1 should have no class"); @@ -173,7 +173,7 @@ add_UITour_task(async function test_info_buttons_2() { ); is( buttons.children[1].getAttribute("image"), - "", + null, "Link should have no image" ); ok( @@ -188,7 +188,7 @@ add_UITour_task(async function test_info_buttons_2() { ); is( buttons.children[2].getAttribute("image"), - "", + null, "First button should have no image" ); diff --git a/browser/components/uitour/test/browser_UITour_defaultBrowser.js b/browser/components/uitour/test/browser_UITour_defaultBrowser.js index 721ab2f8c0..a8572e49ab 100644 --- a/browser/components/uitour/test/browser_UITour_defaultBrowser.js +++ b/browser/components/uitour/test/browser_UITour_defaultBrowser.js @@ -12,10 +12,10 @@ Services.scriptloader.loadSubScript( function MockShellService() {} MockShellService.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIShellService"]), - isDefaultBrowser(aStartupCheck, aForAllTypes) { + isDefaultBrowser() { return false; }, - setDefaultBrowser(aForAllUsers) { + setDefaultBrowser() { setDefaultBrowserCalled = true; }, shouldCheckDefaultBrowser: false, @@ -26,7 +26,7 @@ MockShellService.prototype = { BACKGROUND_FILL: 4, BACKGROUND_FIT: 5, BACKGROUND_SPAN: 6, - setDesktopBackground(aElement, aPosition) {}, + setDesktopBackground() {}, desktopBackgroundColor: 0, }; diff --git a/browser/components/uitour/test/browser_UITour_modalDialog.js b/browser/components/uitour/test/browser_UITour_modalDialog.js index a711ee2f2e..5d1e0a5303 100644 --- a/browser/components/uitour/test/browser_UITour_modalDialog.js +++ b/browser/components/uitour/test/browser_UITour_modalDialog.js @@ -39,7 +39,7 @@ var observer = SpecialPowers.wrapCallbackObject({ return this; }, - observe(subject, topic, data) { + observe() { var doc = getDialogDoc(); if (doc) { handleDialog(doc); diff --git a/browser/components/uitour/test/head.js b/browser/components/uitour/test/head.js index 07b941ba1c..6c7ca00d6e 100644 --- a/browser/components/uitour/test/head.js +++ b/browser/components/uitour/test/head.js @@ -194,14 +194,7 @@ function hideInfoPromise(...args) { * function name to call to generate the buttons/options instead of the * buttons/options themselves. This makes the signature differ from the content one. */ -function showInfoPromise( - target, - title, - text, - icon, - buttonsFunctionName, - optionsFunctionName -) { +function showInfoPromise() { let popup = document.getElementById("UITourTooltip"); let shownPromise = promisePanelElementShown(window, popup); return SpecialPowers.spawn(gTestTab.linkedBrowser, [[...arguments]], args => { @@ -271,7 +264,7 @@ function promisePanelElementEvent(win, aPanel, aEvent) { reject(aEvent + " event did not happen within 5 seconds."); }, 5000); - function onPanelEvent(e) { + function onPanelEvent() { aPanel.removeEventListener(aEvent, onPanelEvent); win.clearTimeout(timeoutId); // Wait one tick to let UITour.sys.mjs process the event as well. @@ -321,7 +314,7 @@ async function loadUITourTestPage(callback, host = "https://example.org/") { // return a function which calls the method of the same name on // contentWin.Mozilla.UITour in a ContentTask. let UITourHandler = { - get(target, prop, receiver) { + get(target, prop) { return (...args) => { let browser = gTestTab.linkedBrowser; // We need to proxy any callback functions using messages: diff --git a/browser/components/urlbar/.eslintrc.js b/browser/components/urlbar/.eslintrc.js index 8ead689bcc..aac2436d20 100644 --- a/browser/components/urlbar/.eslintrc.js +++ b/browser/components/urlbar/.eslintrc.js @@ -5,8 +5,6 @@ "use strict"; module.exports = { - extends: ["plugin:mozilla/require-jsdoc"], - rules: { "mozilla/var-only-at-top-level": "error", "no-unused-expressions": "error", diff --git a/browser/components/urlbar/UrlbarController.sys.mjs b/browser/components/urlbar/UrlbarController.sys.mjs index 9bfc3a645d..7e4d0ff1c5 100644 --- a/browser/components/urlbar/UrlbarController.sys.mjs +++ b/browser/components/urlbar/UrlbarController.sys.mjs @@ -133,6 +133,7 @@ export class UrlbarController { // notifications related to the previous query. this.notify(NOTIFICATIONS.QUERY_STARTED, queryContext); await this.manager.startQuery(queryContext, this); + // If the query has been cancelled, onQueryFinished was notified already. // Note this._lastQueryContextWrapper may have changed in the meanwhile. if ( @@ -144,6 +145,16 @@ export class UrlbarController { this.manager.cancelQuery(queryContext); this.notify(NOTIFICATIONS.QUERY_FINISHED, queryContext); } + + // Record a potential exposure if the current search string matches one of + // the registered keywords. + if (!queryContext.isPrivate) { + let searchStr = queryContext.trimmedLowerCaseSearchString; + if (lazy.UrlbarPrefs.get("potentialExposureKeywords").has(searchStr)) { + this.engagementEvent.addPotentialExposure(searchStr); + } + } + return queryContext; } @@ -335,7 +346,7 @@ export class UrlbarController { } event.preventDefault(); break; - case KeyEvent.DOM_VK_TAB: + case KeyEvent.DOM_VK_TAB: { // It's always possible to tab through results when the urlbar was // focused with the mouse or has a search string, or when the view // already has a selection. @@ -368,6 +379,7 @@ export class UrlbarController { event.preventDefault(); } break; + } case KeyEvent.DOM_VK_PAGE_DOWN: case KeyEvent.DOM_VK_PAGE_UP: if (event.ctrlKey) { @@ -592,8 +604,8 @@ export class UrlbarController { /** * Triggers a "dismiss" engagement for the selected result if one is selected * and it's not the heuristic. Providers that can respond to dismissals of - * their results should implement `onEngagement()`, handle the dismissal, and - * call `controller.removeResult()`. + * their results should implement `onLegacyEngagement()`, handle the + * dismissal, and call `controller.removeResult()`. * * @param {Event} event * The event that triggered dismissal. @@ -783,13 +795,6 @@ class TelemetryEvent { interactionType: this._getStartInteractionType(event, searchString), searchString, }; - - this._controller.manager.notifyEngagementChange( - "start", - queryContext, - {}, - this._controller - ); } /** @@ -821,17 +826,31 @@ class TelemetryEvent { * @param {DOMElement} [details.element] The picked view element. */ record(event, details) { + // Prevent re-entering `record()`. This can happen because + // `#internalRecord()` will notify an engagement to the provider, that may + // execute an action blurring the input field. Then both an engagement + // and an abandonment would be recorded for the same session. + // Nulling out `_startEventInfo` doesn't save us in this case, because it + // happens after `#internalRecord()`, and `isSessionOngoing` must be + // calculated inside it. + if (this.#handlingRecord) { + return; + } + // This should never throw, or it may break the urlbar. try { - this._internalRecord(event, details); + this.#handlingRecord = true; + this.#internalRecord(event, details); } catch (ex) { console.error("Could not record event: ", ex); } finally { + this.#handlingRecord = false; + // Reset the start event info except for engagements that do not end the // search session. In that case, the view stays open and further // engagements are possible and should be recorded when they occur. // (`details.isSessionOngoing` is not a param; rather, it's set by - // `_internalRecord()`.) + // `#internalRecord()`.) if (!details.isSessionOngoing) { this._startEventInfo = null; this._discarded = false; @@ -839,19 +858,10 @@ class TelemetryEvent { } } - _internalRecord(event, details) { + #internalRecord(event, details) { const startEventInfo = this._startEventInfo; if (!this._category || !startEventInfo) { - if (this._discarded && this._category && details?.selType !== "dismiss") { - let { queryContext } = this._controller._lastQueryContextWrapper || {}; - this._controller.manager.notifyEngagementChange( - "discard", - queryContext, - {}, - this._controller - ); - } return; } if ( @@ -938,6 +948,10 @@ class TelemetryEvent { } ); + if (!details.isSessionOngoing) { + this.#recordEndOfSessionTelemetry(details.searchString); + } + if (skipLegacyTelemetry) { this._controller.manager.notifyEngagementChange( method, @@ -1091,23 +1105,6 @@ class TelemetryEvent { return; } - // First check to see if we can record an exposure event - if (method === "abandonment" || method === "engagement") { - if (this.#exposureResultTypes.size) { - let exposure = { - results: [...this.#exposureResultTypes].sort().join(","), - }; - this._controller.logger.debug( - `exposure event: ${JSON.stringify(exposure)}` - ); - Glean.urlbar.exposure.record(exposure); - } - - // reset the provider list on the controller - this.#exposureResultTypes.clear(); - this.#tentativeExposureResultTypes.clear(); - } - this._controller.logger.info( `${method} event: ${JSON.stringify(eventInfo)}` ); @@ -1115,6 +1112,38 @@ class TelemetryEvent { Glean.urlbar[method].record(eventInfo); } + #recordEndOfSessionTelemetry(searchString) { + // exposures + if (this.#exposureResultTypes.size) { + let exposure = { + results: [...this.#exposureResultTypes].sort().join(","), + }; + this._controller.logger.debug( + `exposure event: ${JSON.stringify(exposure)}` + ); + Glean.urlbar.exposure.record(exposure); + this.#exposureResultTypes.clear(); + } + this.#tentativeExposureResultTypes.clear(); + + // potential exposures + if (this.#potentialExposureKeywords.size) { + let normalizedSearchString = searchString.trim().toLowerCase(); + for (let keyword of this.#potentialExposureKeywords) { + let data = { + keyword, + terminal: keyword == normalizedSearchString, + }; + this._controller.logger.debug( + `potential_exposure event: ${JSON.stringify(data)}` + ); + Glean.urlbar.potentialExposure.record(data); + } + GleanPings.urlbarPotentialExposure.submit(); + this.#potentialExposureKeywords.clear(); + } + } + /** * Registers an exposure for a result in the current urlbar session. All * exposures that are added during a session are recorded in an exposure event @@ -1164,6 +1193,16 @@ class TelemetryEvent { this.#tentativeExposureResultTypes.clear(); } + /** + * Registers a potential exposure in the current urlbar session. + * + * @param {string} keyword + * The keyword that was matched. + */ + addPotentialExposure(keyword) { + this.#potentialExposureKeywords.add(keyword); + } + #getInteractionType( method, startEventInfo, @@ -1350,8 +1389,12 @@ class TelemetryEvent { } } + // Used to avoid re-entering `record()`. + #handlingRecord = false; + #previousSearchWordsSet = null; #exposureResultTypes = new Set(); #tentativeExposureResultTypes = new Set(); + #potentialExposureKeywords = new Set(); } diff --git a/browser/components/urlbar/UrlbarInput.sys.mjs b/browser/components/urlbar/UrlbarInput.sys.mjs index 96fc7b9301..a96e862cff 100644 --- a/browser/components/urlbar/UrlbarInput.sys.mjs +++ b/browser/components/urlbar/UrlbarInput.sys.mjs @@ -1364,7 +1364,7 @@ export class UrlbarInput { // The value setter clobbers the actiontype attribute, so we need this // helper to restore it afterwards. const setValueAndRestoreActionType = (value, allowTrim) => { - this._setValue(value, allowTrim); + this._setValue(value, { allowTrim }); switch (result.type) { case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: @@ -1555,7 +1555,7 @@ export class UrlbarInput { !this.value.endsWith(" ") ) { this._autofillPlaceholder = null; - this._setValue(this.window.gBrowser.userTypedValue, false); + this._setValue(this.window.gBrowser.userTypedValue); } return false; @@ -1940,7 +1940,7 @@ export class UrlbarInput { } set value(val) { - this._setValue(val, true); + this._setValue(val, { allowTrim: true }); } get lastSearchString() { @@ -2107,7 +2107,7 @@ export class UrlbarInput { this.searchMode = searchMode; let value = result.payload.query?.trimStart() || ""; - this._setValue(value, false); + this._setValue(value); if (startQuery) { this.startQuery({ allowAutofill: false }); @@ -2253,10 +2253,6 @@ export class UrlbarInput { "--urlbar-height", px(getBoundsWithoutFlushing(this.textbox).height) ); - this.textbox.style.setProperty( - "--urlbar-toolbar-height", - px(getBoundsWithoutFlushing(this._toolbar).height) - ); this.setAttribute("breakout", "true"); this.textbox.parentNode.setAttribute("breakout", "true"); @@ -2266,7 +2262,16 @@ export class UrlbarInput { }); } - _setValue(val, allowTrim) { + /** + * Sets the input field value. + * + * @param {string} val The new value to set. + * @param {object} [options] Options for setting. + * @param {boolean} [options.allowTrim] Whether the value can be trimmed. + * + * @returns {string} The set value. + */ + _setValue(val, { allowTrim = false } = {}) { // Don't expose internal about:reader URLs to the user. let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val); if (originalUrl) { @@ -2730,7 +2735,7 @@ export class UrlbarInput { }) { // The autofilled value may be a URL that includes a scheme at the // beginning. Do not allow it to be trimmed. - this._setValue(value, false); + this._setValue(value); this.inputField.setSelectionRange(selectionStart, selectionEnd); this._autofillPlaceholder = { value, @@ -3152,8 +3157,8 @@ export class UrlbarInput { this.select(); this.window.goDoCommand("cmd_paste"); this.setResultForCurrentValue(null); - this.controller.clearLastQueryContextCache(); this.handleCommand(); + this.controller.clearLastQueryContextCache(); this._suppressStartQuery = false; }); @@ -3504,7 +3509,7 @@ export class UrlbarInput { } if (untrim) { this._focusUntrimmedValue = this._untrimmedValue; - this._setValue(this._focusUntrimmedValue, false); + this._setValue(this._focusUntrimmedValue); } } diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs index 022d0b1c7c..cd8a6b0f4c 100644 --- a/browser/components/urlbar/UrlbarPrefs.sys.mjs +++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs @@ -65,7 +65,7 @@ const PREF_URLBAR_DEFAULTS = new Map([ ["autoFill.stddevMultiplier", [0.0, "float"]], // Feature gate pref for clipboard suggestions in the urlbar. - ["clipboard.featureGate", true], + ["clipboard.featureGate", false], // Whether to show a link for using the search functionality provided by the // active view if the the view utilizes OpenSearch. @@ -1507,12 +1507,28 @@ class Preferences { return this.shouldHandOffToSearchModePrefs.some( prefName => !this.get(prefName) ); - case "autoFillAdaptiveHistoryUseCountThreshold": + case "autoFillAdaptiveHistoryUseCountThreshold": { const nimbusValue = this._nimbus.autoFillAdaptiveHistoryUseCountThreshold; return nimbusValue === undefined ? this.get("autoFill.adaptiveHistory.useCountThreshold") : parseFloat(nimbusValue); + } + case "potentialExposureKeywords": { + // Get the keywords array from Nimbus or prefs and convert it to a Set. + // If the value comes from Nimbus, it will already be an array. If it + // comes from prefs, it should be a stringified array. + let value = this._readPref(pref); + if (typeof value == "string") { + try { + value = JSON.parse(value); + } catch (e) {} + } + if (!Array.isArray(value)) { + value = null; + } + return new Set(value); + } } return this._readPref(pref); } diff --git a/browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs b/browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs index 62f85b2348..be607a80d5 100644 --- a/browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs @@ -49,7 +49,7 @@ class ProviderAboutPages extends UrlbarProvider { * @returns {boolean} Whether this provider should be invoked for the search. */ isActive(queryContext) { - return queryContext.trimmedSearchString.toLowerCase().startsWith("about:"); + return queryContext.trimmedLowerCaseSearchString.startsWith("about:"); } /** @@ -61,7 +61,7 @@ class ProviderAboutPages extends UrlbarProvider { * result. A UrlbarResult should be passed to it. */ startQuery(queryContext, addCallback) { - let searchString = queryContext.trimmedSearchString.toLowerCase(); + let searchString = queryContext.trimmedLowerCaseSearchString; for (const aboutUrl of lazy.AboutPagesUtils.visibleAboutUrls) { if (aboutUrl.startsWith(searchString)) { let result = new lazy.UrlbarResult( diff --git a/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs b/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs index 32e605206e..7470df0fea 100644 --- a/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs @@ -653,7 +653,7 @@ class ProviderAutofill extends UrlbarProvider { queryType: QUERYTYPE.AUTOFILL_ADAPTIVE, // `fullSearchString` is the value the user typed including a prefix if // they typed one. `searchString` has been stripped of the prefix. - fullSearchString: queryContext.searchString.toLowerCase(), + fullSearchString: queryContext.lowerCaseSearchString, searchString: this._searchString, strippedPrefix: this._strippedPrefix, useCountThreshold: lazy.UrlbarPrefs.get( diff --git a/browser/components/urlbar/UrlbarProviderCalculator.sys.mjs b/browser/components/urlbar/UrlbarProviderCalculator.sys.mjs index a55531167c..3f0ffed299 100644 --- a/browser/components/urlbar/UrlbarProviderCalculator.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderCalculator.sys.mjs @@ -157,7 +157,7 @@ class ProviderCalculator extends UrlbarProvider { return viewUpdate; } - onEngagement(state, queryContext, details) { + onLegacyEngagement(state, queryContext, details) { let { result } = details; if (result?.providerName == this.name) { lazy.ClipboardHelper.copyString(result.payload.value); diff --git a/browser/components/urlbar/UrlbarProviderClipboard.sys.mjs b/browser/components/urlbar/UrlbarProviderClipboard.sys.mjs index 5337e610cc..1dc5bb9b86 100644 --- a/browser/components/urlbar/UrlbarProviderClipboard.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderClipboard.sys.mjs @@ -143,10 +143,7 @@ class ProviderClipboard extends UrlbarProvider { addCallback(this, result); } - onEngagement(state, queryContext, details, controller) { - if (!["engagement", "abandonment"].includes(state)) { - return; - } + onLegacyEngagement(state, queryContext, details, controller) { const visibleResults = controller.view?.visibleResults ?? []; for (const result of visibleResults) { if ( diff --git a/browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs b/browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs index 63c94ee8f3..5714f11e72 100644 --- a/browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs @@ -246,7 +246,7 @@ class ProviderContextualSearch extends UrlbarProvider { }; } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { let { result } = details; if (result?.providerName == this.name) { this.#pickResult(result, controller.browserWindow); diff --git a/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs b/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs index f929a1c003..17b6a4c9b0 100644 --- a/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs @@ -200,7 +200,7 @@ class ProviderInputHistory extends UrlbarProvider { } } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { let { result } = details; if (result?.providerName != this.name) { return; @@ -236,7 +236,7 @@ class ProviderInputHistory extends UrlbarProvider { SQL_ADAPTIVE_QUERY, { parent: lazy.PlacesUtils.tagsFolderId, - search_string: queryContext.searchString.toLowerCase(), + search_string: queryContext.lowerCaseSearchString, matchBehavior: Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE, searchBehavior: lazy.UrlbarPrefs.get("defaultBehavior"), userContextId: lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") diff --git a/browser/components/urlbar/UrlbarProviderInterventions.sys.mjs b/browser/components/urlbar/UrlbarProviderInterventions.sys.mjs index 08b4ea36b7..68b9c1665d 100644 --- a/browser/components/urlbar/UrlbarProviderInterventions.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderInterventions.sys.mjs @@ -703,7 +703,7 @@ class ProviderInterventions extends UrlbarProvider { } } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { let { result } = details; // `selType` is "tip" when the tip's main button is picked. Ignore clicks on @@ -714,10 +714,8 @@ class ProviderInterventions extends UrlbarProvider { this.#pickResult(result, controller.browserWindow); } - if (["engagement", "abandonment"].includes(state)) { - for (let tip of this.tipsShownInCurrentEngagement) { - Services.telemetry.keyedScalarAdd("urlbar.tips", `${tip}-shown`, 1); - } + for (let tip of this.tipsShownInCurrentEngagement) { + Services.telemetry.keyedScalarAdd("urlbar.tips", `${tip}-shown`, 1); } this.tipsShownInCurrentEngagement.clear(); } diff --git a/browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs b/browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs index 351e8ff60b..362f683027 100644 --- a/browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs @@ -178,7 +178,7 @@ class ProviderOmnibox extends UrlbarProvider { ); } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { let { result } = details; if (result?.providerName != this.name) { return; diff --git a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs index 650acd1730..c94ebee80a 100644 --- a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs @@ -1517,7 +1517,7 @@ class ProviderPlaces extends UrlbarProvider { search.notifyResult(false); } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { let { result } = details; if (result?.providerName != this.name) { return; diff --git a/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs b/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs index f199b6b892..29370cbaaf 100644 --- a/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs @@ -95,7 +95,7 @@ class ProviderQuickActions extends UrlbarProvider { */ async startQuery(queryContext, addCallback) { await lazy.QuickActionsLoaderDefault.ensureLoaded(); - let input = queryContext.trimmedSearchString.toLowerCase(); + let input = queryContext.trimmedLowerCaseSearchString; if ( !queryContext.searchMode && @@ -241,7 +241,7 @@ class ProviderQuickActions extends UrlbarProvider { } } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { // Ignore engagements on other results that didn't end the session. if (details.result?.providerName != this.name && details.isSessionOngoing) { return; diff --git a/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs b/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs index 78e254616e..fbc8cc8c3f 100644 --- a/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs @@ -229,7 +229,7 @@ class ProviderQuickSuggest extends UrlbarProvider { } } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { // Ignore engagements on other results that didn't end the session. if (details.result?.providerName != this.name && details.isSessionOngoing) { return; @@ -237,7 +237,7 @@ class ProviderQuickSuggest extends UrlbarProvider { // Reset the Merino session ID when a session ends. By design for the user's // privacy, we don't keep it around between engagements. - if (state != "start" && !details.isSessionOngoing) { + if (!details.isSessionOngoing) { this.#merino?.resetSession(); } @@ -486,8 +486,8 @@ class ProviderQuickSuggest extends UrlbarProvider { * end of the engagement or that was dismissed. Null if no quick suggest * result was present. * @param {object} details - * The `details` object that was passed to `onEngagement()`. It must look - * like this: `{ selType, selIndex }` + * The `details` object that was passed to `onLegacyEngagement()`. It must + * look like this: `{ selType, selIndex }` */ #recordEngagement(queryContext, result, details) { let resultSelType = ""; @@ -781,8 +781,8 @@ class ProviderQuickSuggest extends UrlbarProvider { * True if the main part of the result's row was clicked; false if a button * like help or dismiss was clicked or if no part of the row was clicked. * @param {object} options.details - * The `details` object that was passed to `onEngagement()`. It must look - * like this: `{ selType, selIndex }` + * The `details` object that was passed to `onLegacyEngagement()`. It must + * look like this: `{ selType, selIndex }` */ #recordNavSuggestionTelemetry({ queryContext, diff --git a/browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs b/browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs index 48006d09c0..b3c322ffa1 100644 --- a/browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs @@ -188,7 +188,7 @@ class ProviderQuickSuggestContextualOptIn extends UrlbarProvider { row.ownerGlobal.A11yUtils.announce({ raw: alertText }); } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { let { result } = details; if (result?.providerName != this.name) { return; diff --git a/browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs b/browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs index ceeba729d4..1565013440 100644 --- a/browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs @@ -63,7 +63,7 @@ class ProviderRecentSearches extends UrlbarProvider { return 1; } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { let { result } = details; if (result?.providerName != this.name) { return; diff --git a/browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs b/browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs index 8cb3532d94..e3d13feb56 100644 --- a/browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs @@ -352,7 +352,7 @@ class ProviderSearchSuggestions extends UrlbarProvider { return undefined; } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { let { result } = details; if (result?.providerName != this.name) { return; diff --git a/browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs b/browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs index b19528619c..a7a23a3228 100644 --- a/browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs @@ -273,7 +273,7 @@ class ProviderSearchTips extends UrlbarProvider { lazy.UrlbarPrefs.set(`tipShownCount.${tip}`, MAX_SHOWN_COUNT); } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { // Ignore engagements on other results that didn't end the session. let { result } = details; if (result?.providerName != this.name && details.isSessionOngoing) { diff --git a/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs b/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs index 9aabef3d19..0cce6481b1 100644 --- a/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs @@ -194,7 +194,7 @@ class ProviderTabToSearch extends UrlbarProvider { * Called when a result from the provider is selected. "Selected" refers to * the user highlighing the result with the arrow keys/Tab, before it is * picked. onSelection is also called when a user clicks a result. In the - * event of a click, onSelection is called just before onEngagement. + * event of a click, onSelection is called just before onLegacyEngagement. * * @param {UrlbarResult} result * The result that was selected. @@ -226,7 +226,7 @@ class ProviderTabToSearch extends UrlbarProvider { } } - onEngagement(state, queryContext, details) { + onLegacyEngagement(state, queryContext, details) { let { result, element } = details; if ( result?.providerName == this.name && diff --git a/browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs b/browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs index b3a91bcbe4..db9e8df382 100644 --- a/browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs @@ -173,7 +173,7 @@ class ProviderTokenAliasEngines extends UrlbarProvider { } async _getAutofillResult(queryContext) { - let lowerCaseSearchString = queryContext.searchString.toLowerCase(); + let { lowerCaseSearchString } = queryContext; // The user is typing a specific engine. We should show a heuristic result. for (let { engine, tokenAliases } of this._engines) { diff --git a/browser/components/urlbar/UrlbarProviderTopSites.sys.mjs b/browser/components/urlbar/UrlbarProviderTopSites.sys.mjs index e9d968f20f..a046de37d4 100644 --- a/browser/components/urlbar/UrlbarProviderTopSites.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderTopSites.sys.mjs @@ -193,7 +193,7 @@ class ProviderTopSites extends UrlbarProvider { return site; }); - // Store Sponsored Top Sites so we can use it in `onEngagement` + // Store Sponsored Top Sites so we can use it in `onLegacyEngagement` if (sponsoredSites.length) { this.sponsoredSites = sponsoredSites; } @@ -333,12 +333,8 @@ class ProviderTopSites extends UrlbarProvider { } } - onEngagement(state, queryContext) { - if ( - !queryContext.isPrivate && - this.sponsoredSites && - ["engagement", "abandonment"].includes(state) - ) { + onLegacyEngagement(state, queryContext) { + if (!queryContext.isPrivate && this.sponsoredSites) { for (let site of this.sponsoredSites) { Services.telemetry.keyedScalarAdd( SCALAR_CATEGORY_TOPSITES, diff --git a/browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs b/browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs index 98c4d025e4..a5ad28d2aa 100644 --- a/browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs @@ -169,7 +169,7 @@ class ProviderUnitConversion extends UrlbarProvider { addCallback(this, result); } - onEngagement(state, queryContext, details) { + onLegacyEngagement(state, queryContext, details) { let { result, element } = details; if (result?.providerName == this.name) { const { textContent } = element.querySelector( diff --git a/browser/components/urlbar/UrlbarProviderWeather.sys.mjs b/browser/components/urlbar/UrlbarProviderWeather.sys.mjs index 24342fecab..8e9b6b8f3e 100644 --- a/browser/components/urlbar/UrlbarProviderWeather.sys.mjs +++ b/browser/components/urlbar/UrlbarProviderWeather.sys.mjs @@ -115,7 +115,7 @@ class ProviderWeather extends UrlbarProvider { return false; } - return keywords.has(queryContext.searchString.trim().toLocaleLowerCase()); + return keywords.has(queryContext.trimmedLowerCaseSearchString); } /** @@ -163,7 +163,7 @@ class ProviderWeather extends UrlbarProvider { return lazy.QuickSuggest.weather.getViewUpdate(result); } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { // Ignore engagements on other results that didn't end the session. if (details.result?.providerName != this.name && details.isSessionOngoing) { return; @@ -243,7 +243,7 @@ class ProviderWeather extends UrlbarProvider { * A non-empty string means the user picked the weather row or some part of * it, and both impression and click telemetry will be recorded. The * non-empty-string values come from the `details.selType` passed in to - * `onEngagement()`; see `TelemetryEvent.typeFromElement()`. + * `onLegacyEngagement()`; see `TelemetryEvent.typeFromElement()`. */ #recordEngagementTelemetry(result, isPrivate, selType) { // Indexes recorded in quick suggest telemetry are 1-based, so add 1 to the diff --git a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs index 609b0735e1..ac70e03e1b 100644 --- a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs +++ b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs @@ -334,11 +334,11 @@ class ProvidersManager { /** * Notifies all providers when the user starts and ends an engagement with the - * urlbar. For details on parameters, see UrlbarProvider.onEngagement(). + * urlbar. For details on parameters, see + * UrlbarProvider.onLegacyEngagement(). * * @param {string} state - * The state of the engagement, one of: start, engagement, abandonment, - * discard + * The state of the engagement, one of: engagement, abandonment * @param {UrlbarQueryContext} queryContext * The engagement's query context, if available. * @param {object} details @@ -349,7 +349,7 @@ class ProvidersManager { notifyEngagementChange(state, queryContext, details = {}, controller) { for (let provider of this.providers) { provider.tryMethod( - "onEngagement", + "onLegacyEngagement", state, queryContext, details, diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs index 2bbb5d1ab0..9fca8426a3 100644 --- a/browser/components/urlbar/UrlbarUtils.sys.mjs +++ b/browser/components/urlbar/UrlbarUtils.sys.mjs @@ -854,7 +854,7 @@ export var UrlbarUtils = { * @returns {string} The modified paste data. */ stripUnsafeProtocolOnPaste(pasteData) { - while (true) { + for (;;) { let scheme = ""; try { scheme = Services.io.extractScheme(pasteData); @@ -1831,6 +1831,9 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = { isBlockable: { type: "boolean", }, + isManageable: { + type: "boolean", + }, isPinned: { type: "boolean", }, @@ -2175,6 +2178,8 @@ export class UrlbarQueryContext { this.pendingHeuristicProviders = new Set(); this.deferUserSelectionProviders = new Set(); this.trimmedSearchString = this.searchString.trim(); + this.lowerCaseSearchString = this.searchString.toLowerCase(); + this.trimmedLowerCaseSearchString = this.trimmedSearchString.toLowerCase(); this.userContextId = lazy.UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable( options.userContextId, @@ -2431,21 +2436,12 @@ export class UrlbarProvider { * @param {string} _state * The state of the engagement, one of the following strings: * - * start - * A new query has started in the urlbar. * engagement * The user picked a result in the urlbar or used paste-and-go. * abandonment * The urlbar was blurred (i.e., lost focus). - * discard - * This doesn't correspond to a user action, but it means that the - * urlbar has discarded the engagement for some reason, and the - * `onEngagement` implementation should ignore it. - * * @param {UrlbarQueryContext} _queryContext - * The engagement's query context. This is *not* guaranteed to be defined - * when `state` is "start". It will always be defined for "engagement" and - * "abandonment". + * The engagement's query context. * @param {object} _details * This object is non-empty only when `state` is "engagement" or * "abandonment", and it describes the search string and engaged result. @@ -2479,7 +2475,7 @@ export class UrlbarProvider { * @param {UrlbarController} _controller * The associated controller. */ - onEngagement(_state, _queryContext, _details, _controller) {} + onLegacyEngagement(_state, _queryContext, _details, _controller) {} /** * Called before a result from the provider is selected. See `onSelection` @@ -2497,8 +2493,8 @@ export class UrlbarProvider { * Called when a result from the provider is selected. "Selected" refers to * the user highlighing the result with the arrow keys/Tab, before it is * picked. onSelection is also called when a user clicks a result. In the - * event of a click, onSelection is called just before onEngagement. Note that - * this is called when heuristic results are pre-selected. + * event of a click, onSelection is called just before onLegacyEngagement. + * Note that this is called when heuristic results are pre-selected. * * @param {UrlbarResult} _result * The result that was selected. @@ -2581,8 +2577,8 @@ export class UrlbarProvider { /** * Gets the list of commands that should be shown in the result menu for a * given result from the provider. All commands returned by this method should - * be handled by implementing `onEngagement()` with the possible exception of - * commands automatically handled by the urlbar, like "help". + * be handled by implementing `onLegacyEngagement()` with the possible + * exception of commands automatically handled by the urlbar, like "help". * * @param {UrlbarResult} _result * The menu will be shown for this result. @@ -2594,8 +2590,8 @@ export class UrlbarProvider { * {string} name * The name of the command. Must be specified unless `children` is * present. When a command is picked, its name will be passed as - * `details.selType` to `onEngagement()`. The special name "separator" - * will create a menu separator. + * `details.selType` to `onLegacyEngagement()`. The special name + * "separator" will create a menu separator. * {object} l10n * An l10n object for the command's label: `{ id, args }` * Must be specified unless `name` is "separator". diff --git a/browser/components/urlbar/UrlbarView.sys.mjs b/browser/components/urlbar/UrlbarView.sys.mjs index b5fe1e1955..3d6ea46781 100644 --- a/browser/components/urlbar/UrlbarView.sys.mjs +++ b/browser/components/urlbar/UrlbarView.sys.mjs @@ -48,6 +48,7 @@ const ZERO_PREFIX_SCALAR_EXPOSURE = "urlbar.zeroprefix.exposure"; const RESULT_MENU_COMMANDS = { DISMISS: "dismiss", HELP: "help", + MANAGE: "manage", }; const getBoundsWithoutFlushing = element => @@ -3139,6 +3140,15 @@ export class UrlbarView { }, }); } + if (result.payload.isManageable) { + commands.push({ + name: RESULT_MENU_COMMANDS.MANAGE, + l10n: { + id: "urlbar-result-menu-manage-firefox-suggest", + }, + }); + } + let rv = commands.length ? commands : null; this.#resultMenuCommands.set(result, rv); return rv; diff --git a/browser/components/urlbar/docs/dynamic-result-types.rst b/browser/components/urlbar/docs/dynamic-result-types.rst index f72c5e4a13..2c81c1656f 100644 --- a/browser/components/urlbar/docs/dynamic-result-types.rst +++ b/browser/components/urlbar/docs/dynamic-result-types.rst @@ -152,8 +152,8 @@ aren't relevant to dynamic result types, and you should choose values appropriate to your use case. If any elements created in the view for your results can be picked with the -keyboard or mouse, then be sure to implement your provider's ``onEngagement`` -method. +keyboard or mouse, then be sure to implement your provider's +``onLegacyEngagement`` method. For help on implementing providers in general, see the address bar's `Architecture Overview`__. @@ -616,7 +616,7 @@ URL Navigation If a result's payload includes a string ``url`` property and a boolean ``shouldNavigate: true`` property, then picking the result will navigate to the -URL. The ``onEngagement`` method of the result's provider will still be called +URL. The ``onLegacyEngagement`` method of the result's provider will still be called before navigation. Text Highlighting diff --git a/browser/components/urlbar/metrics.yaml b/browser/components/urlbar/metrics.yaml index 95337d84eb..173ee08a10 100644 --- a/browser/components/urlbar/metrics.yaml +++ b/browser/components/urlbar/metrics.yaml @@ -110,7 +110,6 @@ urlbar: `intervention_unknown`, `intervention_update`, `keyword`, - `merino_adm_nonsponsored`, `merino_adm_sponsored`, `merino_amo`, `merino_top_picks`, @@ -159,6 +158,7 @@ urlbar: notification_emails: - fx-search-telemetry@mozilla.com expires: never + engagement: type: event description: Recorded when the user executes an action on a result. @@ -242,7 +242,6 @@ urlbar: `intervention_unknown`, `intervention_update`, `keyword`, - `merino_adm_nonsponsored`, `merino_adm_sponsored`, `merino_amo`, `merino_top_picks`, @@ -363,7 +362,6 @@ urlbar: `intervention_unknown`, `intervention_update`, `keyword`, - `merino_adm_nonsponsored`, `merino_adm_sponsored`, `merino_amo`, `merino_top_picks`, @@ -432,6 +430,40 @@ urlbar: - fx-search-telemetry@mozilla.com expires: never + potential_exposure: + type: event + description: > + This event is recorded in the `urlbar-potential-exposure` ping, which is + submitted at the end of urlbar sessions during which the user typed a + keyword defined by the Nimbus variable `potentialExposureKeywords`. A + "session" begins when the user focuses the urlbar and ends with an + engagement or abandonment. The ping will contain one event per unique + keyword that is typed during the session. This ping is not submitted for + sessions in private windows. + extra_keys: + keyword: + type: string + description: > + The matched keyword. + terminal: + type: boolean + description: > + Whether the matched keyword was present at the end of the urlbar + session. If true, the session ended with the keyword. If false, the + keyword was typed at some point during the session but the session + did not end with it. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1881875 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1881875 + data_sensitivity: + - stored_content + notification_emails: + - fx-search-telemetry@mozilla.com + expires: never + send_in_pings: + - urlbar-potential-exposure + quick_suggest_contextual_opt_in: type: event description: > diff --git a/browser/components/urlbar/pings.yaml b/browser/components/urlbar/pings.yaml index 8153b62863..4c46f16909 100644 --- a/browser/components/urlbar/pings.yaml +++ b/browser/components/urlbar/pings.yaml @@ -19,3 +19,19 @@ quick-suggest: - https://bugzilla.mozilla.org/show_bug.cgi?id=1854755 notification_emails: - najiang@mozilla.com + +urlbar-potential-exposure: + description: | + This ping is submitted at the end of urlbar sessions during which the user + typed a keyword defined by the Nimbus variable `potentialExposureKeywords`. + A "session" begins when the user focuses the urlbar and ends with an + engagement or abandonment. The ping will contain one + `urlbar.potential_exposure` event per unique keyword that is typed during + the session. This ping is not submitted for sessions in private windows. + include_client_id: false + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1881875 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1881875 + notification_emails: + - fx-search-telemetry@mozilla.com diff --git a/browser/components/urlbar/private/AddonSuggestions.sys.mjs b/browser/components/urlbar/private/AddonSuggestions.sys.mjs index 23311cec1c..ace82e41d3 100644 --- a/browser/components/urlbar/private/AddonSuggestions.sys.mjs +++ b/browser/components/urlbar/private/AddonSuggestions.sys.mjs @@ -21,7 +21,7 @@ const UTM_PARAMS = { }; const RESULT_MENU_COMMAND = { - HELP: "help", + MANAGE: "manage", NOT_INTERESTED: "not_interested", NOT_RELEVANT: "not_relevant", SHOW_LESS_FREQUENTLY: "show_less_frequently", @@ -212,9 +212,9 @@ export class AddonSuggestions extends BaseFeature { }, { name: "separator" }, { - name: RESULT_MENU_COMMAND.HELP, + name: RESULT_MENU_COMMAND.MANAGE, l10n: { - id: "urlbar-result-menu-learn-more-about-firefox-suggest", + id: "urlbar-result-menu-manage-firefox-suggest", }, } ); @@ -224,8 +224,8 @@ export class AddonSuggestions extends BaseFeature { handleCommand(view, result, selType) { switch (selType) { - case RESULT_MENU_COMMAND.HELP: - // "help" is handled by UrlbarInput, no need to do anything here. + case RESULT_MENU_COMMAND.MANAGE: + // "manage" is handled by UrlbarInput, no need to do anything here. break; // selType == "dismiss" when the user presses the dismiss key shortcut. case "dismiss": diff --git a/browser/components/urlbar/private/AdmWikipedia.sys.mjs b/browser/components/urlbar/private/AdmWikipedia.sys.mjs index 3ab5bad09f..596e15df4c 100644 --- a/browser/components/urlbar/private/AdmWikipedia.sys.mjs +++ b/browser/components/urlbar/private/AdmWikipedia.sys.mjs @@ -190,14 +190,11 @@ export class AdmWikipedia extends BaseFeature { sponsoredBlockId: suggestion.block_id, sponsoredAdvertiser: suggestion.advertiser, sponsoredIabCategory: suggestion.iab_category, - helpUrl: lazy.QuickSuggest.HELP_URL, - helpL10n: { - id: "urlbar-result-menu-learn-more-about-firefox-suggest", - }, isBlockable: true, blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest", }, + isManageable: true, }; let result = new lazy.UrlbarResult( diff --git a/browser/components/urlbar/private/MDNSuggestions.sys.mjs b/browser/components/urlbar/private/MDNSuggestions.sys.mjs index c9e7da18af..3efedbd12a 100644 --- a/browser/components/urlbar/private/MDNSuggestions.sys.mjs +++ b/browser/components/urlbar/private/MDNSuggestions.sys.mjs @@ -15,7 +15,7 @@ ChromeUtils.defineESModuleGetters(lazy, { }); const RESULT_MENU_COMMAND = { - HELP: "help", + MANAGE: "manage", NOT_INTERESTED: "not_interested", NOT_RELEVANT: "not_relevant", }; @@ -157,9 +157,9 @@ export class MDNSuggestions extends BaseFeature { }, { name: "separator" }, { - name: RESULT_MENU_COMMAND.HELP, + name: RESULT_MENU_COMMAND.MANAGE, l10n: { - id: "urlbar-result-menu-learn-more-about-firefox-suggest", + id: "urlbar-result-menu-manage-firefox-suggest", }, }, ]; @@ -167,8 +167,8 @@ export class MDNSuggestions extends BaseFeature { handleCommand(view, result, selType) { switch (selType) { - case RESULT_MENU_COMMAND.HELP: - // "help" is handled by UrlbarInput, no need to do anything here. + case RESULT_MENU_COMMAND.MANAGE: + // "manage" is handled by UrlbarInput, no need to do anything here. break; // selType == "dismiss" when the user presses the dismiss key shortcut. case "dismiss": diff --git a/browser/components/urlbar/private/SuggestBackendRust.sys.mjs b/browser/components/urlbar/private/SuggestBackendRust.sys.mjs index 2d96e7540f..3993149757 100644 --- a/browser/components/urlbar/private/SuggestBackendRust.sys.mjs +++ b/browser/components/urlbar/private/SuggestBackendRust.sys.mjs @@ -136,11 +136,12 @@ export class SuggestBackendRust extends BaseFeature { suggestion.provider = type; suggestion.is_sponsored = type == "Amp" || type == "Yelp"; if (Array.isArray(suggestion.icon)) { - suggestion.icon_blob = new Blob( - [new Uint8Array(suggestion.icon)], - type == "Yelp" ? { type: "image/svg+xml" } : null - ); + suggestion.icon_blob = new Blob([new Uint8Array(suggestion.icon)], { + type: suggestion.iconMimetype ?? "", + }); + delete suggestion.icon; + delete suggestion.iconMimetype; } } diff --git a/browser/components/urlbar/private/YelpSuggestions.sys.mjs b/browser/components/urlbar/private/YelpSuggestions.sys.mjs index 4cf454c71d..e2a2803bd7 100644 --- a/browser/components/urlbar/private/YelpSuggestions.sys.mjs +++ b/browser/components/urlbar/private/YelpSuggestions.sys.mjs @@ -15,8 +15,8 @@ ChromeUtils.defineESModuleGetters(lazy, { }); const RESULT_MENU_COMMAND = { - HELP: "help", INACCURATE_LOCATION: "inaccurate_location", + MANAGE: "manage", NOT_INTERESTED: "not_interested", NOT_RELEVANT: "not_relevant", SHOW_LESS_FREQUENTLY: "show_less_frequently", @@ -168,9 +168,9 @@ export class YelpSuggestions extends BaseFeature { }, { name: "separator" }, { - name: RESULT_MENU_COMMAND.HELP, + name: RESULT_MENU_COMMAND.MANAGE, l10n: { - id: "urlbar-result-menu-learn-more-about-firefox-suggest", + id: "urlbar-result-menu-manage-firefox-suggest", }, } ); @@ -180,8 +180,8 @@ export class YelpSuggestions extends BaseFeature { handleCommand(view, result, selType, searchString) { switch (selType) { - case RESULT_MENU_COMMAND.HELP: - // "help" is handled by UrlbarInput, no need to do anything here. + case RESULT_MENU_COMMAND.MANAGE: + // "manage" is handled by UrlbarInput, no need to do anything here. break; case RESULT_MENU_COMMAND.INACCURATE_LOCATION: // Currently the only way we record this feedback is in the Glean diff --git a/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs index cfc9ecb3d8..f576f4ca19 100644 --- a/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs +++ b/browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs @@ -158,7 +158,7 @@ export var UrlbarTestUtils = { lazy.UrlbarPrefs.get("trimURLs") && value != lazy.BrowserUIUtils.trimURL(value) ) { - window.gURLBar._setValue(value, false); + window.gURLBar._setValue(value); fireInputEvent = true; } else { window.gURLBar.value = value; @@ -1315,10 +1315,7 @@ export var UrlbarTestUtils = { // Set most of the string directly instead of going through sendString, // so that we don't make life unnecessarily hard for consumers by // possibly starting multiple searches. - win.gURLBar._setValue( - text.substr(0, text.length - 1), - false /* allowTrim = */ - ); + win.gURLBar._setValue(text.substr(0, text.length - 1)); } this.EventUtils.sendString(text.substr(-1, 1), win); }, @@ -1490,7 +1487,7 @@ class TestProvider extends UrlbarProvider { * @param {Function} [options.onSelection] * If given, a function that will be called when * {@link UrlbarView.#selectElement} method is called. - * @param {Function} [options.onEngagement] + * @param {Function} [options.onLegacyEngagement] * If given, a function that will be called when engagement. * @param {Function} [options.delayResultsPromise] * If given, we'll await on this before returning results. @@ -1503,7 +1500,7 @@ class TestProvider extends UrlbarProvider { addTimeout = 0, onCancel = null, onSelection = null, - onEngagement = null, + onLegacyEngagement = null, delayResultsPromise = null, } = {}) { if (delayResultsPromise && addTimeout) { @@ -1520,7 +1517,7 @@ class TestProvider extends UrlbarProvider { this._type = type; this._onCancel = onCancel; this._onSelection = onSelection; - this._onEngagement = onEngagement; + this._onLegacyEngagement = onLegacyEngagement; // As this has been a common source of mistakes, auto-upgrade the provider // type to heuristic if any result is heuristic. @@ -1574,8 +1571,8 @@ class TestProvider extends UrlbarProvider { this._onSelection?.(result, element); } - onEngagement(state, queryContext, details, controller) { - this._onEngagement?.(state, queryContext, details, controller); + onLegacyEngagement(state, queryContext, details, controller) { + this._onLegacyEngagement?.(state, queryContext, details, controller); } } diff --git a/browser/components/urlbar/tests/browser-tips/browser_picks.js b/browser/components/urlbar/tests/browser-tips/browser_picks.js index ba0ff69357..c9d725dfb5 100644 --- a/browser/components/urlbar/tests/browser-tips/browser_picks.js +++ b/browser/components/urlbar/tests/browser-tips/browser_picks.js @@ -117,8 +117,8 @@ async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) { }); UrlbarProvidersManager.registerProvider(provider); - let onEngagementPromise = new Promise( - resolve => (provider.onEngagement = resolve) + let onLegacyEngagementPromise = new Promise( + resolve => (provider.onLegacyEngagement = resolve) ); // Do a search to show our tip result. @@ -142,8 +142,8 @@ async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) { ); } - // Now pick the target and wait for provider.onEngagement to be called and - // the URL to load if necessary. + // Now pick the target and wait for provider.onLegacyEngagement to be called + // and the URL to load if necessary. let loadPromise; if (buttonUrl || helpUrl) { loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); @@ -160,7 +160,7 @@ async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) { EventUtils.synthesizeKey("KEY_Enter"); } }); - await onEngagementPromise; + await onLegacyEngagementPromise; await loadPromise; // Check telemetry. diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js index a82a2d658b..8c98e27993 100644 --- a/browser/components/urlbar/tests/browser-tips/browser_searchTips.js +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js @@ -18,7 +18,7 @@ ChromeUtils.defineESModuleGetters(this, { "resource:///modules/UrlbarProviderSearchTips.sys.mjs", }); -// These should match the same consts in UrlbarProviderSearchTips.jsm. +// These should match the same consts in UrlbarProviderSearchTips.sys.mjs. const MAX_SHOWN_COUNT = 4; const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000; diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js index 72d05cf632..6c0550a2df 100644 --- a/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js @@ -25,7 +25,7 @@ XPCOMUtils.defineLazyServiceGetter( "nsIClipboardHelper" ); -// These should match the same consts in UrlbarProviderSearchTips.jsm. +// These should match the same consts in UrlbarProviderSearchTips.sys.mjs. const MAX_SHOWN_COUNT = 4; const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000; diff --git a/browser/components/urlbar/tests/browser/browser.toml b/browser/components/urlbar/tests/browser/browser.toml index b9934aa838..44b964e5ca 100644 --- a/browser/components/urlbar/tests/browser/browser.toml +++ b/browser/components/urlbar/tests/browser/browser.toml @@ -4,7 +4,11 @@ support-files = [ "head.js", "head-common.js", ] - +skip-if = [ + "os == 'linux' && os_version == '18.04' && asan", # long running manifest + "os == 'linux' && os_version == '18.04' && tsan", # long running manifest + "win11_2009 && asan", # long running manifest +] prefs = [ "browser.bookmarks.testing.skipDefaultBookmarksImport=true", "browser.urlbar.trending.featureGate=false", @@ -280,6 +284,8 @@ support-files = [ ["browser_keyword_select_and_type.js"] +["browser_less_common_selection_manipulations.js"] + ["browser_loadRace.js"] ["browser_locationBarCommand.js"] @@ -398,9 +404,6 @@ https_first_disabled = true ["browser_revert.js"] -["browser_search_continuation.js"] -support-files = ["search-engines", "../../../search/test/browser/trendingSuggestionEngine.sjs"] - ["browser_searchFunction.js"] ["browser_searchHistoryLimit.js"] @@ -490,6 +493,9 @@ support-files = [ ["browser_search_bookmarks_from_bookmarks_menu.js"] +["browser_search_continuation.js"] +support-files = ["search-engines", "../../../search/test/browser/trendingSuggestionEngine.sjs"] + ["browser_search_history_from_history_panel.js"] ["browser_selectStaleResults.js"] diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js index f191cae321..d01734959a 100644 --- a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js @@ -2,7 +2,7 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ -async function testVal(aExpected, overflowSide = "") { +async function testVal(aExpected, overflowSide = null) { info(`Testing ${aExpected}`); try { gURLBar.setURI(makeURI(aExpected)); diff --git a/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js index 427a7419c8..bb710c7065 100644 --- a/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js +++ b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js @@ -98,7 +98,7 @@ add_task(async function clearURLBarAfterManuallyLoadingAboutHome() { () => {} ); // This opens about:newtab: - BrowserOpenTab(); + BrowserCommands.openTab(); let tab = await promiseTabOpenedAndSwitchedTo; is(gURLBar.value, "", "URL bar should be empty"); is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null"); @@ -132,7 +132,7 @@ add_task(async function dontTemporarilyShowAboutHome() { let win = OpenBrowserWindow(); await windowOpenedPromise; let promiseTabSwitch = BrowserTestUtils.switchTab(win.gBrowser, () => {}); - win.BrowserOpenTab(); + win.BrowserCommands.openTab(); await promiseTabSwitch; currentBrowser = win.gBrowser.selectedBrowser; is(win.gBrowser.visibleTabs.length, 2, "2 tabs opened"); diff --git a/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js index 8c4b05501e..54f40a85ee 100644 --- a/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js +++ b/browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js @@ -389,11 +389,11 @@ class TestProvider extends UrlbarTestUtils.TestProvider { ]; } - onEngagement(state, queryContext, details, controller) { + onLegacyEngagement(state, queryContext, details, controller) { if (details.result?.providerName == this.name) { let { selType } = details; - info(`onEngagement called, selType=` + selType); + info(`onLegacyEngagement called, selType=` + selType); if (!this.commandCount.hasOwnProperty(selType)) { this.commandCount[selType] = 0; diff --git a/browser/components/urlbar/tests/browser/browser_copy_during_load.js b/browser/components/urlbar/tests/browser/browser_copy_during_load.js index 3eaa53bcda..e1d352a171 100644 --- a/browser/components/urlbar/tests/browser/browser_copy_during_load.js +++ b/browser/components/urlbar/tests/browser/browser_copy_during_load.js @@ -45,7 +45,7 @@ add_task(async function () { null, true ); - BrowserStop(); + BrowserCommands.stop(); await browserStoppedPromise; }); }); diff --git a/browser/components/urlbar/tests/browser/browser_dynamicResults.js b/browser/components/urlbar/tests/browser/browser_dynamicResults.js index aad15e0145..2ba1b7ab5f 100644 --- a/browser/components/urlbar/tests/browser/browser_dynamicResults.js +++ b/browser/components/urlbar/tests/browser/browser_dynamicResults.js @@ -511,7 +511,7 @@ add_task(async function shouldNavigate() { await UrlbarTestUtils.promisePopupClose(window, () => EventUtils.synthesizeKey("KEY_Enter") ); - // Verify that onEngagement was still called. + // Verify that onLegacyEngagement was still called. let [result, pickedElement] = await pickPromise; Assert.equal(result, row.result, "Picked result"); Assert.equal(pickedElement, element, "Picked element"); @@ -904,7 +904,7 @@ class TestProvider extends UrlbarTestUtils.TestProvider { }; } - onEngagement(state, queryContext, details, _controller) { + onLegacyEngagement(state, queryContext, details, _controller) { if (this._pickPromiseResolve) { let { result, element } = details; this._pickPromiseResolve([result, element]); diff --git a/browser/components/urlbar/tests/browser/browser_engagement.js b/browser/components/urlbar/tests/browser/browser_engagement.js index b1998b6f55..fbc321e322 100644 --- a/browser/components/urlbar/tests/browser/browser_engagement.js +++ b/browser/components/urlbar/tests/browser/browser_engagement.js @@ -1,7 +1,7 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests the UrlbarProvider.onEngagement() method. +// Tests the UrlbarProvider.onLegacyEngagement() method. "use strict"; @@ -110,32 +110,21 @@ async function doTest({ let provider = new TestProvider(); UrlbarProvidersManager.registerProvider(provider); - let startPromise = provider.promiseEngagement(); await UrlbarTestUtils.promiseAutocompleteResultPopup({ window: win, value: "test", fireInputEvent: true, }); - let [state, queryContext, details, controller] = await startPromise; - Assert.equal( - controller.input.isPrivate, - expectedIsPrivate, - "Start isPrivate" - ); - Assert.equal(state, "start", "Start state"); - - // `queryContext` isn't always defined for `start`, and `onEngagement` - // shouldn't rely on it being defined on start, but there's no good reason to - // assert that it's not defined here. - - // Similarly, `details` is never defined for `start`, but there's no good - // reason to assert that it's not defined. - let endPromise = provider.promiseEngagement(); let { result, element } = (await endEngagement()) ?? {}; - [state, queryContext, details, controller] = await endPromise; + let [state, queryContext, details, controller] = await endPromise; + + Assert.ok( + ["engagement", "abandonment"].includes(state), + "State should be either 'engagement' or 'abandonment'" + ); Assert.equal(controller.input.isPrivate, expectedIsPrivate, "End isPrivate"); Assert.equal(state, expectedEndState, "End state"); Assert.ok(queryContext, "End queryContext"); @@ -179,7 +168,7 @@ async function doTest({ } /** - * Test provider that resolves promises when onEngagement is called. + * Test provider that resolves promises when onLegacyEngagement is called. */ class TestProvider extends UrlbarTestUtils.TestProvider { _resolves = []; @@ -197,7 +186,7 @@ class TestProvider extends UrlbarTestUtils.TestProvider { }); } - onEngagement(...args) { + onLegacyEngagement(...args) { let resolve = this._resolves.shift(); if (resolve) { resolve(args); diff --git a/browser/components/urlbar/tests/browser/browser_less_common_selection_manipulations.js b/browser/components/urlbar/tests/browser/browser_less_common_selection_manipulations.js new file mode 100644 index 0000000000..2ad6ee0e07 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_less_common_selection_manipulations.js @@ -0,0 +1,288 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests less common mouse/keyboard manipulations of the address bar input + * field selection, for example: + * - Home/Del + * - Shift+Right/Left + * - Drag selection + * - Double-click on word + * + * All the tests set up some initial conditions, and check it. Then optionally + * they can manipulate the selection further, and check the results again. + * We want to ensure the final selection is the expected one, even if in the + * future we change our trimming strategy for the input field value. + */ + +const tests = [ + { + description: "Test HOME starting from full selection", + openPanel() { + EventUtils.synthesizeKey("l", { accelKey: true }); + }, + get selection() { + return [0, gURLBar.value.length]; + }, + manipulate() { + // Cursor must move to the first visible character, regardless of any + // "untrimming" we could be doing. + this._visibleValue = gURLBar.value; + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true }); + } else { + EventUtils.synthesizeKey("KEY_Home"); + } + }, + get modifiedSelection() { + let start = gURLBar.value.indexOf(this._visibleValue); + return [start, start]; + }, + }, + { + description: "Test END starting from full selection", + openPanel() { + EventUtils.synthesizeKey("l", { accelKey: true }); + }, + get selection() { + return [0, gURLBar.value.length]; + }, + manipulate() { + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_ArrowRight", { metaKey: true }); + } else { + EventUtils.synthesizeKey("KEY_End", {}); + } + }, + get modifiedSelection() { + return [gURLBar.value.length, gURLBar.value.length]; + }, + }, + { + description: "Test SHIFT+LEFT starting from full selection", + openPanel() { + EventUtils.synthesizeKey("l", { accelKey: true }); + }, + get selection() { + return [0, gURLBar.value.length]; + }, + manipulate() { + EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }); + }, + get modifiedSelection() { + return [0, gURLBar.value.length - 1]; + }, + }, + { + description: "Test SHIFT+RIGHT starting from full selection", + openPanel() { + EventUtils.synthesizeKey("l", { accelKey: true }); + }, + get selection() { + return [0, gURLBar.value.length]; + }, + manipulate() { + EventUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }); + }, + get modifiedSelection() { + return [0, gURLBar.value.length]; + }, + }, + { + description: "Test Drag Selection from the first character", + async openPanel() { + this._expectedSelectedText = gURLBar.value.substring(0, 5); + await selectWithMouseDrag( + getTextWidth(gURLBar.value[0]) / 2 - 1, + getTextWidth(gURLBar.value.substring(0, 5)) + ); + }, + get selection() { + return [ + 0, + gURLBar.value.indexOf(this._expectedSelectedText) + + this._expectedSelectedText.length, + ]; + }, + }, + { + description: "Test Drag Selection from the last character", + async openPanel() { + this._expectedSelectedText = gURLBar.value.substring(-5); + await selectWithMouseDrag( + getTextWidth(gURLBar.value) + 1, + getTextWidth(this._expectedSelectedText) + ); + }, + get selection() { + return [ + gURLBar.value.indexOf(this._expectedSelectedText), + gURLBar.value.length, + ]; + }, + }, + { + description: "Test Drag Selection in the middle of the string", + async openPanel() { + this._expectedSelectedText = gURLBar.value.substring(5, 10); + await selectWithMouseDrag( + getTextWidth(gURLBar.value.substring(0, 5)), + getTextWidth(gURLBar.value.substring(0, 10)) + ); + }, + get selection() { + let start = gURLBar.value.indexOf(this._expectedSelectedText); + return [start, start + this._expectedSelectedText.length]; + }, + }, + { + description: "Test Double-click on word", + async openPanel() { + let wordBoundaryIndex = gURLBar.value.search(/\btest/); + this._expectedSelectedText = "test"; + await selectWithDoubleClick( + getTextWidth(gURLBar.value.substring(0, wordBoundaryIndex)) + ); + }, + get selection() { + let start = gURLBar.value.indexOf(this._expectedSelectedText); + return [start, start + this._expectedSelectedText.length]; + }, + }, + { + description: "Click at the right of the text", + openPanel() { + EventUtils.synthesizeKey("l", { accelKey: true }); + }, + get selection() { + return [0, gURLBar.value.length]; + }, + manipulate() { + let rect = gURLBar.inputField.getBoundingClientRect(); + EventUtils.synthesizeMouse( + gURLBar.inputField, + getTextWidth(gURLBar.value) + 10, + rect.height / 2, + {} + ); + }, + get modifiedSelection() { + return [gURLBar.value.length, gURLBar.value.length]; + }, + }, +]; + +add_setup(async function () { + gURLBar.inputField.style.font = "14px monospace"; + registerCleanupFunction(() => { + gURLBar.inputField.style.font = null; + }); +}); + +add_task(async function https() { + await doTest("https://example.com/test/some/page.htm"); +}); + +add_task(async function http() { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await doTest("http://example.com/test/other/page.htm"); +}); + +async function doTest(url) { + await BrowserTestUtils.withNewTab(url, async () => { + for (let test of tests) { + gURLBar.blur(); + info(test.description); + await UrlbarTestUtils.promisePopupOpen(window, async () => { + await test.openPanel(); + }); + info( + `Selected text is <${gURLBar.value.substring( + gURLBar.selectionStart, + gURLBar.selectionEnd + )}>` + ); + Assert.deepEqual( + test.selection, + [gURLBar.selectionStart, gURLBar.selectionEnd], + "Check selection" + ); + + if (test.manipulate) { + await test.manipulate(); + info( + `Selected text is <${gURLBar.value.substring( + gURLBar.selectionStart, + gURLBar.selectionEnd + )}>` + ); + Assert.deepEqual( + test.modifiedSelection, + [gURLBar.selectionStart, gURLBar.selectionEnd], + "Check selection after manipulation" + ); + } + } + }); +} + +function getTextWidth(inputText) { + const canvas = + getTextWidth.canvas || + (getTextWidth.canvas = document.createElement("canvas")); + let context = canvas.getContext("2d"); + context.font = window + .getComputedStyle(gURLBar.inputField) + .getPropertyValue("font"); + return context.measureText(inputText).width; +} + +function selectWithMouseDrag(fromX, toX) { + let target = gURLBar.inputField; + let rect = target.getBoundingClientRect(); + let promise = BrowserTestUtils.waitForEvent(target, "mouseup"); + EventUtils.synthesizeMouse( + target, + fromX, + rect.height / 2, + { type: "mousemove" }, + target.ownerGlobal + ); + EventUtils.synthesizeMouse( + target, + fromX, + rect.height / 2, + { type: "mousedown" }, + target.ownerGlobal + ); + EventUtils.synthesizeMouse( + target, + toX, + rect.height / 2, + { type: "mousemove" }, + target.ownerGlobal + ); + EventUtils.synthesizeMouse( + target, + toX, + rect.height / 2, + { type: "mouseup" }, + target.ownerGlobal + ); + return promise; +} + +function selectWithDoubleClick(offsetX) { + let target = gURLBar.inputField; + let rect = target.getBoundingClientRect(); + let promise = BrowserTestUtils.waitForEvent(target, "dblclick"); + EventUtils.synthesizeMouse(target, offsetX, rect.height / 2, { + clickCount: 1, + }); + EventUtils.synthesizeMouse(target, offsetX, rect.height / 2, { + clickCount: 2, + }); + return promise; +} diff --git a/browser/components/urlbar/tests/browser/browser_locationBarCommand.js b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js index 84c45e586a..92409f979f 100644 --- a/browser/components/urlbar/tests/browser/browser_locationBarCommand.js +++ b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js @@ -276,7 +276,7 @@ async function typeAndCommand(eventType, details = {}) { async function triggerCommand(eventType, details = {}) { Assert.equal( await UrlbarTestUtils.promiseUserContextId(window), - gBrowser.selectedTab.getAttribute("usercontextid"), + gBrowser.selectedTab.getAttribute("usercontextid") || "", "userContextId must be the same as the originating tab" ); diff --git a/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js index 2f8e871bfe..66a8ed3a41 100644 --- a/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js +++ b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js @@ -42,7 +42,7 @@ add_task(async function () { // tab button. let userInput = window.windowUtils.setHandlingUserInput(true); try { - BrowserOpenTab(); + BrowserCommands.openTab(); } finally { userInput.destruct(); } diff --git a/browser/components/urlbar/tests/browser/browser_raceWithTabs.js b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js index 17560ea101..821aa0f0ee 100644 --- a/browser/components/urlbar/tests/browser/browser_raceWithTabs.js +++ b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js @@ -41,7 +41,7 @@ add_task(async function hitEnterLoadInRightTab() { gBrowser.tabContainer, "TabOpen" ); - BrowserOpenTab(); + BrowserCommands.openTab(); let oldTab = (await oldTabOpenPromise).target; let oldTabLoadedPromise = BrowserTestUtils.browserLoaded( oldTab.linkedBrowser, @@ -60,7 +60,7 @@ add_task(async function hitEnterLoadInRightTab() { EventUtils.sendKey("return"); info("Immediately open a second tab"); - BrowserOpenTab(); + BrowserCommands.openTab(); let newTab = (await tabOpenPromise).target; info("Created new tab; waiting for tabs to load"); diff --git a/browser/components/urlbar/tests/browser/browser_result_menu.js b/browser/components/urlbar/tests/browser/browser_result_menu.js index ccbe247598..f00b92fa63 100644 --- a/browser/components/urlbar/tests/browser/browser_result_menu.js +++ b/browser/components/urlbar/tests/browser/browser_result_menu.js @@ -201,10 +201,10 @@ add_task(async function firefoxSuggest() { ], }); - // Implement the provider's `onEngagement()` so it removes the result. - let onEngagementCallCount = 0; - provider.onEngagement = (state, queryContext, details, controller) => { - onEngagementCallCount++; + // Implement the provider's `onLegacyEngagement()` so it removes the result. + let onLegacyEngagementCallCount = 0; + provider.onLegacyEngagement = (state, queryContext, details, controller) => { + onLegacyEngagementCallCount++; controller.removeResult(details.result); }; @@ -245,9 +245,9 @@ add_task(async function firefoxSuggest() { }); Assert.greater( - onEngagementCallCount, + onLegacyEngagementCallCount, 0, - "onEngagement() should have been called" + "onLegacyEngagement() should have been called" ); Assert.equal( UrlbarTestUtils.getResultCount(window), diff --git a/browser/components/urlbar/tests/browser/browser_stop_pending.js b/browser/components/urlbar/tests/browser/browser_stop_pending.js index 50f5dfdeec..938a57dc28 100644 --- a/browser/components/urlbar/tests/browser/browser_stop_pending.js +++ b/browser/components/urlbar/tests/browser/browser_stop_pending.js @@ -125,7 +125,7 @@ add_task(async function () { null, true ); - BrowserStop(); + BrowserCommands.stop(); await browserStoppedPromise; is( @@ -207,7 +207,7 @@ add_task(async function () { null, true ); - BrowserStop(); + BrowserCommands.stop(); await browserStoppedPromise; is( diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js index 318b29ad19..b2591a0c14 100644 --- a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js @@ -344,8 +344,8 @@ async function impressions_test(isOnboarding) { 5 ); - // See javadoc for UrlbarProviderTabToSearch.onEngagement for discussion - // about retained results. + // See javadoc for UrlbarProviderTabToSearch.onLegacyEngagement for + // discussion about retained results. info("Reopen the result set with retained results. Record impression."); await UrlbarTestUtils.promisePopupOpen(window, () => { EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml index cf6bc80318..a72f2d9b8d 100644 --- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml @@ -26,8 +26,6 @@ prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"] ["browser_glean_telemetry_abandonment_n_chars_n_words.js"] -["browser_glean_telemetry_abandonment_type.js"] - ["browser_glean_telemetry_abandonment_sap.js"] ["browser_glean_telemetry_abandonment_search_engine_default_id.js"] @@ -36,6 +34,8 @@ prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"] ["browser_glean_telemetry_abandonment_tips.js"] +["browser_glean_telemetry_abandonment_type.js"] + ["browser_glean_telemetry_engagement_edge_cases.js"] ["browser_glean_telemetry_engagement_groups.js"] @@ -66,4 +66,8 @@ skip-if = ["verify"] # Bug 1852375 - MerinoTestUtils.initWeather() doesn't play ["browser_glean_telemetry_exposure_edge_cases.js"] +["browser_glean_telemetry_potential_exposure.js"] + ["browser_glean_telemetry_record_preferences.js"] + +["browser_glean_telemetry_reenter.js"] diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_type.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_type.js index 99145d7cc3..b8a16bd10c 100644 --- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_type.js +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_type.js @@ -19,7 +19,7 @@ function checkUrlbarFocus(win, focusState) { // URL bar records the correct abandonment telemetry with abandonment type // "tab_swtich". add_task(async function tabSwitchFocusedToFocused() { - await doTest(async browser => { + await doTest(async () => { await UrlbarTestUtils.promiseAutocompleteResultPopup({ window, value: "test search", @@ -45,7 +45,7 @@ add_task(async function tabSwitchFocusedToFocused() { // URL bar loses focus logs abandonment telemetry with abandonment type // "blur". add_task(async function tabSwitchFocusedToUnfocused() { - await doTest(async browser => { + await doTest(async () => { await UrlbarTestUtils.promiseAutocompleteResultPopup({ window, value: "test search", @@ -65,7 +65,7 @@ add_task(async function tabSwitchFocusedToUnfocused() { // the URL bar gains focus does not record any abandonment telemetry, reflecting // no change in focus state relevant to abandonment. add_task(async function tabSwitchUnFocusedToFocused() { - await doTest(async browser => { + await doTest(async () => { checkUrlbarFocus(window, false); let promiseTabOpened = BrowserTestUtils.waitForEvent( @@ -91,7 +91,7 @@ add_task(async function tabSwitchUnFocusedToFocused() { // Checks that switching between two tabs, both with unfocused URL bars, does // not trigger any abandonment telmetry. add_task(async function tabSwitchUnFocusedToUnFocused() { - await doTest(async browser => { + await doTest(async () => { checkUrlbarFocus(window, false); let tab2 = await BrowserTestUtils.openNewForegroundTab(window.gBrowser); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js index ff31bdc52a..053d307088 100644 --- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js @@ -63,7 +63,7 @@ add_task(async function selected_result_tip() { ), ], priority: 1, - onEngagement: () => { + onLegacyEngagement: () => { deferred.resolve(); }, }); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js index 6b1dedbce2..59c4460e52 100644 --- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js @@ -101,11 +101,32 @@ add_task(async function engagement_type_dismiss() { }); add_task(async function engagement_type_help() { - const cleanupQuickSuggest = await ensureQuickSuggestInit(); + const url = "https://example.com/"; + const helpUrl = "https://example.com/help"; + let provider = new UrlbarTestUtils.TestProvider({ + priority: Infinity, + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url, + isBlockable: true, + blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest" }, + helpUrl, + helpL10n: { + id: "urlbar-result-menu-learn-more-about-firefox-suggest", + }, + } + ), + ], + }); + UrlbarProvidersManager.registerProvider(provider); await doTest(async () => { - await openPopup("sponsored"); - await selectRowByURL("https://example.com/sponsored"); + await openPopup("test"); + await selectRowByURL(url); + const onTabOpened = BrowserTestUtils.waitForNewTab(gBrowser); UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "L"); const tab = await onTabOpened; @@ -114,5 +135,26 @@ add_task(async function engagement_type_help() { assertEngagementTelemetry([{ engagement_type: "help" }]); }); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +add_task(async function engagement_type_manage() { + const cleanupQuickSuggest = await ensureQuickSuggestInit(); + + await doTest(async () => { + await openPopup("sponsored"); + await selectRowByURL("https://example.com/sponsored"); + + const onManagePageLoaded = BrowserTestUtils.browserLoaded( + browser, + false, + "about:preferences#search" + ); + UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "M"); + await onManagePageLoaded; + + assertEngagementTelemetry([{ engagement_type: "manage" }]); + }); + await cleanupQuickSuggest(); }); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js index 07e8b9b360..ef2ec623bc 100644 --- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js @@ -11,7 +11,7 @@ add_setup(async function () { await initExposureTest(); }); -add_task(async function exposureSponsoredOnEngagement() { +add_task(async function exposureSponsoredOnLegacyEngagement() { await doExposureTest({ prefs: [ ["browser.urlbar.exposureResults", suggestResultType("adm_sponsored")], diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_potential_exposure.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_potential_exposure.js new file mode 100644 index 0000000000..275e3968eb --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_potential_exposure.js @@ -0,0 +1,438 @@ +/* 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/. */ + +// Tests the `urlbar-potential-exposure` ping. + +const WAIT_FOR_PING_TIMEOUT_MS = 1000; + +// Avoid timeouts in verify mode, especially on Mac. +requestLongerTimeout(3); + +add_setup(async function test_setup() { + Services.fog.testResetFOG(); + + // Add a mock engine so we don't hit the network. + await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); + + registerCleanupFunction(() => { + Services.fog.testResetFOG(); + }); +}); + +add_task(async function oneKeyword_noMatch_1() { + await doTest({ + keywords: ["example"], + searchStrings: ["exam"], + expectedEvents: [], + }); +}); + +add_task(async function oneKeyword_noMatch_2() { + await doTest({ + keywords: ["exam"], + searchStrings: ["example"], + expectedEvents: [], + }); +}); + +add_task(async function oneKeyword_oneMatch_terminal_1() { + await doTest({ + keywords: ["example"], + searchStrings: ["example"], + expectedEvents: [{ extra: { keyword: "example", terminal: true } }], + }); +}); + +add_task(async function oneKeyword_oneMatch_terminal_2() { + await doTest({ + keywords: ["example"], + searchStrings: ["exam", "example"], + expectedEvents: [{ extra: { keyword: "example", terminal: true } }], + }); +}); + +add_task(async function oneKeyword_oneMatch_nonterminal_1() { + await doTest({ + keywords: ["example"], + searchStrings: ["example", "exam"], + expectedEvents: [{ extra: { keyword: "example", terminal: false } }], + }); +}); + +add_task(async function oneKeyword_oneMatch_nonterminal_2() { + await doTest({ + keywords: ["example"], + searchStrings: ["ex", "example", "exam"], + expectedEvents: [{ extra: { keyword: "example", terminal: false } }], + }); +}); + +add_task(async function oneKeyword_dupeMatches_terminal_1() { + await doTest({ + keywords: ["example"], + searchStrings: ["example", "example"], + expectedEvents: [{ extra: { keyword: "example", terminal: true } }], + }); +}); + +add_task(async function oneKeyword_dupeMatches_terminal_2() { + await doTest({ + keywords: ["example"], + searchStrings: ["example", "exampl", "example"], + expectedEvents: [{ extra: { keyword: "example", terminal: true } }], + }); +}); + +add_task(async function oneKeyword_dupeMatches_terminal_3() { + await doTest({ + keywords: ["example"], + searchStrings: ["exam", "example", "example"], + expectedEvents: [{ extra: { keyword: "example", terminal: true } }], + }); +}); + +add_task(async function oneKeyword_dupeMatches_terminal_4() { + await doTest({ + keywords: ["example"], + searchStrings: ["exam", "example", "exampl", "example"], + expectedEvents: [{ extra: { keyword: "example", terminal: true } }], + }); +}); + +add_task(async function oneKeyword_dupeMatches_nonterminal_1() { + await doTest({ + keywords: ["example"], + searchStrings: ["example", "example", "exampl"], + expectedEvents: [{ extra: { keyword: "example", terminal: false } }], + }); +}); + +add_task(async function oneKeyword_dupeMatches_nonterminal_2() { + await doTest({ + keywords: ["example"], + searchStrings: ["exam", "example", "example", "exampl"], + expectedEvents: [{ extra: { keyword: "example", terminal: false } }], + }); +}); + +add_task(async function oneKeyword_dupeMatches_nonterminal_3() { + await doTest({ + keywords: ["example"], + searchStrings: ["example", "exam", "example", "exampl"], + expectedEvents: [{ extra: { keyword: "example", terminal: false } }], + }); +}); + +add_task(async function oneKeyword_dupeMatches_nonterminal_4() { + await doTest({ + keywords: ["example"], + searchStrings: ["exam", "example", "exampl", "example", "exampl"], + expectedEvents: [{ extra: { keyword: "example", terminal: false } }], + }); +}); + +add_task(async function manyKeywords_noMatch() { + await doTest({ + keywords: ["foo", "bar", "baz"], + searchStrings: ["example"], + expectedEvents: [], + }); +}); + +add_task(async function manyKeywords_oneMatch_terminal_1() { + await doTest({ + keywords: ["foo", "bar", "baz"], + searchStrings: ["bar"], + expectedEvents: [{ extra: { keyword: "bar", terminal: true } }], + }); +}); + +add_task(async function manyKeywords_oneMatch_terminal_2() { + await doTest({ + keywords: ["foo", "bar", "baz"], + searchStrings: ["example", "bar"], + expectedEvents: [{ extra: { keyword: "bar", terminal: true } }], + }); +}); + +add_task(async function manyKeywords_oneMatch_nonterminal_1() { + await doTest({ + keywords: ["foo", "bar", "baz"], + searchStrings: ["bar", "example"], + expectedEvents: [{ extra: { keyword: "bar", terminal: false } }], + }); +}); + +add_task(async function manyKeywords_oneMatch_nonterminal_2() { + await doTest({ + keywords: ["foo", "bar", "baz"], + searchStrings: ["exam", "bar", "example"], + expectedEvents: [{ extra: { keyword: "bar", terminal: false } }], + }); +}); + +add_task(async function manyKeywords_manyMatches_terminal_1() { + let keywords = ["foo", "bar", "baz"]; + await doTest({ + keywords, + searchStrings: keywords, + expectedEvents: keywords.map((keyword, i) => ({ + extra: { keyword, terminal: i == keywords.length - 1 }, + })), + }); +}); + +add_task(async function manyKeywords_manyMatches_terminal_2() { + let keywords = ["foo", "bar", "baz"]; + await doTest({ + keywords, + searchStrings: ["exam", "foo", "exampl", "bar", "example", "baz"], + expectedEvents: keywords.map((keyword, i) => ({ + extra: { keyword, terminal: i == keywords.length - 1 }, + })), + }); +}); + +add_task(async function manyKeywords_manyMatches_nonterminal_1() { + let keywords = ["foo", "bar", "baz"]; + await doTest({ + keywords, + searchStrings: ["foo", "bar", "baz", "example"], + expectedEvents: keywords.map(keyword => ({ + extra: { keyword, terminal: false }, + })), + }); +}); + +add_task(async function manyKeywords_manyMatches_nonterminal_2() { + let keywords = ["foo", "bar", "baz"]; + await doTest({ + keywords, + searchStrings: ["exam", "foo", "exampl", "bar", "example", "baz", "exam"], + expectedEvents: keywords.map(keyword => ({ + extra: { keyword, terminal: false }, + })), + }); +}); + +add_task(async function manyKeywords_dupeMatches_terminal() { + let keywords = ["foo", "bar", "baz"]; + let searchStrings = [...keywords, ...keywords]; + await doTest({ + keywords, + searchStrings, + expectedEvents: keywords.map((keyword, i) => ({ + extra: { keyword, terminal: i == keywords.length - 1 }, + })), + }); +}); + +add_task(async function manyKeywords_dupeMatches_nonterminal() { + let keywords = ["foo", "bar", "baz"]; + let searchStrings = [...keywords, ...keywords, "example"]; + await doTest({ + keywords, + searchStrings, + expectedEvents: keywords.map(keyword => ({ + extra: { keyword, terminal: false }, + })), + }); +}); + +add_task(async function searchStringNormalization_terminal() { + await doTest({ + keywords: ["example"], + searchStrings: [" ExaMPLe "], + expectedEvents: [{ extra: { keyword: "example", terminal: true } }], + }); +}); + +add_task(async function searchStringNormalization_nonterminal() { + await doTest({ + keywords: ["example"], + searchStrings: [" ExaMPLe ", "foo"], + expectedEvents: [{ extra: { keyword: "example", terminal: false } }], + }); +}); + +add_task(async function multiWordKeyword() { + await doTest({ + keywords: ["this has multiple words"], + searchStrings: ["this has multiple words"], + expectedEvents: [ + { extra: { keyword: "this has multiple words", terminal: true } }, + ], + }); +}); + +// Smoke test that ends a session with an engagement instead of an abandonment +// as other tasks in this file do. +add_task(async function engagement() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + await doTest({ + keywords: ["example"], + searchStrings: ["example"], + endSession: () => + // Hit the Enter key on the heuristic search result. + UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Enter") + ), + expectedEvents: [{ extra: { keyword: "example", terminal: true } }], + }); + }); +}); + +// Smoke test that uses Nimbus to set keywords instead of a pref as other tasks +// in this file do. +add_task(async function nimbus() { + let keywords = ["foo", "bar", "baz"]; + await doTest({ + useNimbus: true, + keywords, + searchStrings: keywords, + expectedEvents: keywords.map((keyword, i) => ({ + extra: { keyword, terminal: i == keywords.length - 1 }, + })), + }); +}); + +// The ping should not be submitted for sessions in private windows. +add_task(async function privateWindow() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await doTest({ + win: privateWin, + keywords: ["example"], + searchStrings: ["example"], + expectedEvents: [], + }); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function invalidPotentialExposureKeywords_pref() { + await doTest({ + keywords: "not an array of keywords", + searchStrings: ["example", "not an array of keywords"], + expectedEvents: [], + }); +}); + +add_task(async function invalidPotentialExposureKeywords_nimbus() { + await doTest({ + useNimbus: true, + keywords: "not an array of keywords", + searchStrings: ["example", "not an array of keywords"], + expectedEvents: [], + }); +}); + +async function doTest({ + keywords, + searchStrings, + expectedEvents, + endSession = null, + useNimbus = false, + win = window, +}) { + endSession ||= () => + UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur()); + + let nimbusCleanup; + let keywordsJson = JSON.stringify(keywords); + if (useNimbus) { + nimbusCleanup = await UrlbarTestUtils.initNimbusFeature({ + potentialExposureKeywords: keywordsJson, + }); + } else { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.potentialExposureKeywords", keywordsJson]], + }); + } + + let pingPromise = waitForPing(); + + for (let value of searchStrings) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value, + window: win, + }); + } + await endSession(); + + // Wait `WAIT_FOR_PING_TIMEOUT_MS` for the ping to be submitted before + // reporting a timeout. Note that some tasks do not expect a ping to be + // submitted, and they rely on this timeout behavior. + info("Awaiting ping promise"); + let events = null; + events = await Promise.race([ + pingPromise, + new Promise(resolve => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(() => { + if (!events) { + info("Timed out waiting for ping"); + } + resolve([]); + }, WAIT_FOR_PING_TIMEOUT_MS) + ), + ]); + + assertEvents(events, expectedEvents); + + if (nimbusCleanup) { + await nimbusCleanup(); + } else { + await SpecialPowers.popPrefEnv(); + } + Services.fog.testResetFOG(); +} + +function waitForPing() { + return new Promise(resolve => { + GleanPings.urlbarPotentialExposure.testBeforeNextSubmit(() => { + let events = Glean.urlbar.potentialExposure.testGetValue(); + info("testBeforeNextSubmit got events: " + JSON.stringify(events)); + resolve(events); + }); + }); +} + +function assertEvents(actual, expected) { + info("Comparing events: " + JSON.stringify({ actual, expected })); + + // Add some expected boilerplate properties to the expected events so that + // callers don't have to but so that we still check them. + expected = expected.map(e => ({ + category: "urlbar", + name: "potential_exposure", + // `testGetValue()` stringifies booleans for some reason. Let callers + // specify booleans since booleans are correct, and stringify them here. + ...stringifyBooleans(e), + })); + + // Filter out properties from the actual events that aren't defined in the + // expected events. Ignore unimportant properties like timestamps. + actual = actual.map((a, i) => + Object.fromEntries( + Object.entries(a).filter(([key]) => expected[i]?.hasOwnProperty(key)) + ) + ); + + Assert.deepEqual(actual, expected, "Checking expected Glean events"); +} + +function stringifyBooleans(obj) { + let newObj = {}; + for (let [key, value] of Object.entries(obj)) { + if (value && typeof value == "object") { + newObj[key] = stringifyBooleans(value); + } else if (typeof value == "boolean") { + newObj[key] = String(value); + } else { + newObj[key] = value; + } + } + return newObj; +} diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_reenter.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_reenter.js new file mode 100644 index 0000000000..51bdc84870 --- /dev/null +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_reenter.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test we don't re-enter record() (and record both an engagement and an +// abandonment) when handling an engagement blurs the input field. + +const TEST_URL = "https://example.com/"; + +add_task(async function () { + await setup(); + let deferred = Promise.withResolvers(); + const provider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + url: TEST_URL, + helpUrl: "https://example.com/help", + helpL10n: { + id: "urlbar-result-menu-tip-get-help", + }, + } + ), + ], + priority: 999, + onLegacyEngagement: () => { + info("Blur the address bar during the onLegacyEngagement notification"); + gURLBar.blur(); + // Run at the next tick to be sure spurious events would have happened. + TestUtils.waitForTick().then(() => { + deferred.resolve(); + }); + }, + }); + UrlbarProvidersManager.registerProvider(provider); + // This should cover at least engagement and abandonment. + let engagementSpy = sinon.spy(provider, "onLegacyEngagement"); + + let beforeRecordCall = false, + recordReentered = false; + let recordStub = sinon + .stub(gURLBar.controller.engagementEvent, "record") + .callsFake((...args) => { + recordReentered = beforeRecordCall; + beforeRecordCall = true; + recordStub.wrappedMethod.apply(gURLBar.controller.engagementEvent, args); + beforeRecordCall = false; + }); + + registerCleanupFunction(() => { + sinon.restore(); + UrlbarProvidersManager.unregisterProvider(provider); + }); + + await doTest(async () => { + await openPopup("example"); + await selectRowByURL(TEST_URL); + EventUtils.synthesizeKey("VK_RETURN"); + await deferred.promise; + + assertEngagementTelemetry([{ engagement_type: "enter" }]); + assertAbandonmentTelemetry([]); + + Assert.ok(recordReentered, "`record()` was re-entered"); + Assert.equal( + engagementSpy.callCount, + 1, + "`onLegacyEngagement` was invoked twice" + ); + Assert.equal( + engagementSpy.args[0][0], + "engagement", + "`engagement` notified" + ); + }); +}); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js index 4317a50930..1373cc7e27 100644 --- a/browser/components/urlbar/tests/engagementTelemetry/browser/head.js +++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js @@ -10,6 +10,7 @@ Services.scriptloader.loadSubScript( ChromeUtils.defineESModuleGetters(this, { QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", }); const lazy = {}; @@ -210,12 +211,7 @@ async function doTest(testFn) { await QuickSuggest.blockedSuggestions.clear(); await QuickSuggest.blockedSuggestions._test_readyPromise; await updateTopSites(() => true); - - try { - await BrowserTestUtils.withNewTab(gBrowser, testFn); - } catch (e) { - console.error(e); - } + await BrowserTestUtils.withNewTab(gBrowser, testFn); } async function initGroupTest() { diff --git a/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs index 2ba9dce8be..1002b4e231 100644 --- a/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs +++ b/browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs @@ -490,6 +490,8 @@ class _QuickSuggestTestUtils { * Whether the result is expected to be sponsored. * @param {boolean} [options.isBestMatch] * Whether the result is expected to be a best match. + * @param {boolean} [options.isManageable] + * Whether the result is expected to show Manage result menu item. * @returns {result} * The quick suggest result. */ @@ -500,6 +502,7 @@ class _QuickSuggestTestUtils { index = -1, isSponsored = true, isBestMatch = false, + isManageable = true, } = {}) { this.Assert.ok( url || originalUrl, @@ -574,11 +577,19 @@ class _QuickSuggestTestUtils { } this.Assert.equal( - result.payload.helpUrl, - lazy.QuickSuggest.HELP_URL, - "Result helpURL" + result.payload.isManageable, + isManageable, + "Result isManageable" ); + if (!isManageable) { + this.Assert.equal( + result.payload.helpUrl, + lazy.QuickSuggest.HELP_URL, + "Result helpURL" + ); + } + this.Assert.ok( row._buttons.get("menu"), "The menu button should be present" diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js index 130afe8c53..98f6ba6117 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js @@ -164,3 +164,40 @@ add_tasks_with_rust( await cleanUpNimbus(); } ); + +// Tests the "Manage" result menu for sponsored suggestion. +add_tasks_with_rust(async function resultMenu_manage_sponsored() { + await BrowserTestUtils.withNewTab({ gBrowser }, async browser => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "fra", + }); + + const managePage = "about:preferences#search"; + let onManagePageLoaded = BrowserTestUtils.browserLoaded( + browser, + false, + managePage + ); + // Click the command. + await UrlbarTestUtils.openResultMenuAndClickItem(window, "manage", { + resultIndex: 1, + }); + await onManagePageLoaded; + Assert.equal( + browser.currentURI.spec, + managePage, + "The manage page is loaded" + ); + + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests the "Manage" result menu for non-sponsored suggestion. +add_tasks_with_rust(async function resultMenu_manage_nonSponsored() { + await doManageTest({ + input: "nonspon", + index: 1, + }); +}); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js index b09345aa54..f34b479134 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js @@ -245,10 +245,15 @@ add_task(async function resultMenu_notInterested() { }); // Tests the "Not relevant" result menu dismissal command. -add_task(async function notRelevant() { +add_task(async function resultMenu_notRelevant() { await doDismissTest("not_relevant", false); }); +// Tests the "Manage" result menu. +add_task(async function resultMenu_manage() { + await doManageTest({ input: "only match the Merino suggestion", index: 1 }); +}); + // Tests the row/group label. add_task(async function rowLabel() { await UrlbarTestUtils.promiseAutocompleteResultPopup({ diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js index c400cf72f6..3fa91e5a32 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js @@ -5,11 +5,6 @@ "use strict"; -ChromeUtils.defineESModuleGetters(this, { - CONTEXTUAL_SERVICES_PING_TYPES: - "resource:///modules/PartnerLinkAttribution.sys.mjs", -}); - const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; const { TIMESTAMP_TEMPLATE } = QuickSuggest; diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js index b7da7533c4..33bd37703d 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js @@ -21,6 +21,9 @@ const REMOTE_SETTINGS_DATA = [ }, ]; +// Avoid timeouts in verify mode. They're especially common on Mac. +requestLongerTimeout(5); + add_setup(async function () { await QuickSuggestTestUtils.ensureQuickSuggestInit({ remoteSettingsRecords: REMOTE_SETTINGS_DATA, @@ -28,35 +31,37 @@ add_setup(async function () { }); add_tasks_with_rust(async function basic() { - const suggestion = REMOTE_SETTINGS_DATA[0].attachment[0]; - await UrlbarTestUtils.promiseAutocompleteResultPopup({ - window, - value: suggestion.keywords[0], - }); - Assert.equal(UrlbarTestUtils.getResultCount(window), 2); - - const { element, result } = await UrlbarTestUtils.getDetailsOfResultAt( - window, - 1 - ); - Assert.equal( - result.providerName, - UrlbarProviderQuickSuggest.name, - "The result should be from the expected provider" - ); - Assert.equal( - result.payload.provider, - UrlbarPrefs.get("quickSuggestRustEnabled") ? "Mdn" : "MDNSuggestions" - ); + await BrowserTestUtils.withNewTab("about:blank", async () => { + const suggestion = REMOTE_SETTINGS_DATA[0].attachment[0]; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: suggestion.keywords[0], + }); + Assert.equal(UrlbarTestUtils.getResultCount(window), 2); + + const { element, result } = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 1 + ); + Assert.equal( + result.providerName, + UrlbarProviderQuickSuggest.name, + "The result should be from the expected provider" + ); + Assert.equal( + result.payload.provider, + UrlbarPrefs.get("quickSuggestRustEnabled") ? "Mdn" : "MDNSuggestions" + ); - const onLoad = BrowserTestUtils.browserLoaded( - gBrowser.selectedBrowser, - false, - result.payload.url - ); - EventUtils.synthesizeMouseAtCenter(element.row, {}); - await onLoad; - Assert.ok(true, "Expected page is loaded"); + const onLoad = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + result.payload.url + ); + EventUtils.synthesizeMouseAtCenter(element.row, {}); + await onLoad; + Assert.ok(true, "Expected page is loaded"); + }); await PlacesUtils.history.clear(); }); @@ -111,7 +116,7 @@ add_tasks_with_rust(async function resultMenu_notInterested() { }); // Tests the "Not relevant" result menu dismissal command. -add_tasks_with_rust(async function notRelevant() { +add_tasks_with_rust(async function resultMenu_notRelevant() { await doDismissTest("not_relevant"); Assert.equal(UrlbarPrefs.get("suggest.mdn"), true); @@ -123,6 +128,11 @@ add_tasks_with_rust(async function notRelevant() { await QuickSuggest.blockedSuggestions.clear(); }); +// Tests the "Manage" result menu. +add_tasks_with_rust(async function resultMenu_manage() { + await doManageTest({ input: "array", index: 1 }); +}); + async function doDismissTest(command) { const keyword = REMOTE_SETTINGS_DATA[0].attachment[0].keywords[0]; // Do a search. diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js index 0064b6a297..a40a35893b 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js @@ -4,12 +4,6 @@ "use strict"; // Browser tests for Pocket suggestions. -// -// TODO: Make this work with Rust enabled. Right now, running this test with -// Rust hits the following error on ingest, which prevents ingest from finishing -// successfully: -// -// 0:03.17 INFO Console message: [JavaScript Error: "1698289045697 urlbar ERROR QuickSuggest.SuggestBackendRust :: Ingest error: Error executing SQL: FOREIGN KEY constraint failed" {file: "resource://gre/modules/Log.sys.mjs" line: 722}] // The expected index of the Pocket suggestion. const EXPECTED_RESULT_INDEX = 1; @@ -30,6 +24,8 @@ const REMOTE_SETTINGS_DATA = [ }, ]; +requestLongerTimeout(5); + add_setup(async function () { await SpecialPowers.pushPrefEnv({ set: [ @@ -47,7 +43,7 @@ add_setup(async function () { }); }); -add_task(async function basic() { +add_tasks_with_rust(async function basic() { await BrowserTestUtils.withNewTab("about:blank", async () => { // Do a search. await UrlbarTestUtils.promiseAutocompleteResultPopup({ @@ -96,7 +92,7 @@ add_task(async function basic() { }); // Tests the "Show less frequently" command. -add_task(async function resultMenu_showLessFrequently() { +add_tasks_with_rust(async function resultMenu_showLessFrequently() { await SpecialPowers.pushPrefEnv({ set: [ ["browser.urlbar.pocket.featureGate", true], @@ -235,7 +231,7 @@ async function doShowLessFrequently({ input, expected, keepViewOpen = false }) { } // Tests the "Not interested" result menu dismissal command. -add_task(async function resultMenu_notInterested() { +add_tasks_with_rust(async function resultMenu_notInterested() { await doDismissTest("not_interested"); // Re-enable suggestions and wait until PocketSuggestions syncs them from @@ -245,7 +241,7 @@ add_task(async function resultMenu_notInterested() { }); // Tests the "Not relevant" result menu dismissal command. -add_task(async function notRelevant() { +add_tasks_with_rust(async function notRelevant() { await doDismissTest("not_relevant"); }); @@ -361,7 +357,7 @@ async function doDismissTest(command) { } // Tests row labels. -add_task(async function rowLabel() { +add_tasks_with_rust(async function rowLabel() { const testCases = [ // high confidence keyword best match { @@ -389,7 +385,7 @@ add_task(async function rowLabel() { }); // Tests visibility of "Show less frequently" menu. -add_task(async function showLessFrequentlyMenuVisibility() { +add_tasks_with_rust(async function showLessFrequentlyMenuVisibility() { const testCases = [ // high confidence keyword best match { diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js index b7c2bdc25c..7197946171 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js @@ -401,6 +401,11 @@ async function doDismiss({ menu, assert }) { await UrlbarTestUtils.promisePopupClose(window); } +// Tests the "Manage" result menu. +add_task(async function resultMenu_manage() { + await doManageTest({ input: "ramen", index: 1 }); +}); + // Tests the row/group label. add_task(async function rowLabel() { let tests = [ diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js index 001c54458c..71c289e0ef 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js @@ -7,11 +7,6 @@ "use strict"; -ChromeUtils.defineESModuleGetters(this, { - CONTEXTUAL_SERVICES_PING_TYPES: - "resource:///modules/PartnerLinkAttribution.sys.mjs", -}); - const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; const MERINO_SUGGESTION = { diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js index 00cbe6c4e1..2c75b63a71 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js @@ -7,11 +7,6 @@ "use strict"; -ChromeUtils.defineESModuleGetters(this, { - CONTEXTUAL_SERVICES_PING_TYPES: - "resource:///modules/PartnerLinkAttribution.sys.mjs", -}); - const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; const MERINO_RESULT = { diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js index 821c5cf470..eab48faaaf 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js @@ -8,8 +8,6 @@ "use strict"; ChromeUtils.defineESModuleGetters(this, { - CONTEXTUAL_SERVICES_PING_TYPES: - "resource:///modules/PartnerLinkAttribution.sys.mjs", UrlbarView: "resource:///modules/UrlbarView.sys.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", }); @@ -376,8 +374,11 @@ async function doEngagementWithoutAddingResultToView( let getPriorityStub = sandbox.stub(UrlbarProviderQuickSuggest, "getPriority"); getPriorityStub.returns(Infinity); - // Spy on `UrlbarProviderQuickSuggest.onEngagement()`. - let onEngagementSpy = sandbox.spy(UrlbarProviderQuickSuggest, "onEngagement"); + // Spy on `UrlbarProviderQuickSuggest.onLegacyEngagement()`. + let onLegacyEngagementSpy = sandbox.spy( + UrlbarProviderQuickSuggest, + "onLegacyEngagement" + ); let sandboxCleanup = () => { getPriorityStub?.restore(); @@ -454,7 +455,7 @@ async function doEngagementWithoutAddingResultToView( }); await loadPromise; - let engagementCalls = onEngagementSpy.getCalls().filter(call => { + let engagementCalls = onLegacyEngagementSpy.getCalls().filter(call => { let state = call.args[0]; return state == "engagement"; }); diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js index 9a1aa06c02..f541801bae 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js @@ -7,11 +7,6 @@ "use strict"; -ChromeUtils.defineESModuleGetters(this, { - CONTEXTUAL_SERVICES_PING_TYPES: - "resource:///modules/PartnerLinkAttribution.sys.mjs", -}); - const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; const REMOTE_SETTINGS_RESULT = { diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js index 7c477e8af7..b11a491c92 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js @@ -7,11 +7,6 @@ "use strict"; -ChromeUtils.defineESModuleGetters(this, { - CONTEXTUAL_SERVICES_PING_TYPES: - "resource:///modules/PartnerLinkAttribution.sys.mjs", -}); - const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest; const REMOTE_SETTINGS_RESULT = { diff --git a/browser/components/urlbar/tests/quicksuggest/browser/head.js b/browser/components/urlbar/tests/quicksuggest/browser/head.js index cc5f449e94..a1bf0feabe 100644 --- a/browser/components/urlbar/tests/quicksuggest/browser/head.js +++ b/browser/components/urlbar/tests/quicksuggest/browser/head.js @@ -12,7 +12,7 @@ Services.scriptloader.loadSubScript( ChromeUtils.defineESModuleGetters(this, { CONTEXTUAL_SERVICES_PING_TYPES: - "resource:///modules/PartnerLinkAttribution.jsm", + "resource:///modules/PartnerLinkAttribution.sys.mjs", QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", UrlbarProviderQuickSuggest: @@ -522,6 +522,45 @@ async function doCommandTest({ info("Finished command test: " + JSON.stringify({ commandOrArray })); } +/* + * Do test the "Manage" result menu item. + * + * @param {object} options + * Options + * @param {number} options.index + * The index of the suggestion that will be checked in the results list. + * @param {number} options.input + * The input value on the urlbar. + */ +async function doManageTest({ index, input }) { + await BrowserTestUtils.withNewTab({ gBrowser }, async browser => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + }); + + const managePage = "about:preferences#search"; + let onManagePageLoaded = BrowserTestUtils.browserLoaded( + browser, + false, + managePage + ); + // Click the command. + await UrlbarTestUtils.openResultMenuAndClickItem(window, "manage", { + resultIndex: index, + }); + await onManagePageLoaded; + + Assert.equal( + browser.currentURI.spec, + managePage, + "The manage page is loaded" + ); + + await UrlbarTestUtils.promisePopupClose(window); + }); +} + /** * Gets a row in the view, which is assumed to be open, and asserts that it's a * particular quick suggest row. If it is, the row is returned. If it's not, diff --git a/browser/components/urlbar/tests/quicksuggest/unit/head.js b/browser/components/urlbar/tests/quicksuggest/unit/head.js index 73bedf468e..5808e06bdf 100644 --- a/browser/components/urlbar/tests/quicksuggest/unit/head.js +++ b/browser/components/urlbar/tests/quicksuggest/unit/head.js @@ -182,14 +182,11 @@ function makeWikipediaResult({ qsSuggestion: keyword, sponsoredAdvertiser: "Wikipedia", sponsoredIabCategory: "5 - Education", - helpUrl: QuickSuggest.HELP_URL, - helpL10n: { - id: "urlbar-result-menu-learn-more-about-firefox-suggest", - }, isBlockable: true, blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest", }, + isManageable: true, telemetryType: "adm_nonsponsored", }, }; @@ -256,14 +253,11 @@ function makeAmpResult({ sponsoredBlockId: blockId, sponsoredAdvertiser: advertiser, sponsoredIabCategory: iabCategory, - helpUrl: QuickSuggest.HELP_URL, - helpL10n: { - id: "urlbar-result-menu-learn-more-about-firefox-suggest", - }, isBlockable: true, blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest", }, + isManageable: true, telemetryType: "adm_sponsored", descriptionL10n: { id: "urlbar-result-action-sponsored" }, }, diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js index 1c00cb5320..ecb7c3dd09 100644 --- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js @@ -3884,7 +3884,7 @@ async function checkSearch({ name, searchString, expectedResults }) { removeResult() {}, }, }); - UrlbarProviderQuickSuggest.onEngagement( + UrlbarProviderQuickSuggest.onLegacyEngagement( "engagement", context, { diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js index 61b1b9186f..c98fc5b6b4 100644 --- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js @@ -149,7 +149,7 @@ add_task(async function canceledQueries() { }); function endEngagement({ controller, context = null, state = "engagement" }) { - UrlbarProviderQuickSuggest.onEngagement( + UrlbarProviderQuickSuggest.onLegacyEngagement( state, context || createContext("endEngagement", { diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js index cd794f435b..8479b97210 100644 --- a/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js +++ b/browser/components/urlbar/tests/quicksuggest/unit/test_weather.js @@ -723,7 +723,7 @@ add_tasks_with_rust(async function block() { let result = context.results[0]; let provider = UrlbarProvidersManager.getProvider(result.providerName); Assert.ok(provider, "Sanity check: Result provider found"); - provider.onEngagement( + provider.onLegacyEngagement( "engagement", context, { diff --git a/browser/components/urlbar/tests/unit/test_exposure.js b/browser/components/urlbar/tests/unit/test_exposure.js index e3ce0b8479..3e63e668d7 100644 --- a/browser/components/urlbar/tests/unit/test_exposure.js +++ b/browser/components/urlbar/tests/unit/test_exposure.js @@ -3,7 +3,6 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ ChromeUtils.defineESModuleGetters(this, { - QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs", UrlbarProviderQuickSuggest: "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs", }); @@ -177,14 +176,11 @@ function makeAmpResult({ sponsoredBlockId: blockId, sponsoredAdvertiser: advertiser, sponsoredIabCategory: iabCategory, - helpUrl: QuickSuggest.HELP_URL, - helpL10n: { - id: "urlbar-result-menu-learn-more-about-firefox-suggest", - }, isBlockable: true, blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest", }, + isManageable: true, telemetryType: "adm_sponsored", descriptionL10n: { id: "urlbar-result-action-sponsored" }, }, @@ -240,14 +236,11 @@ function makeWikipediaResult({ qsSuggestion: keyword, sponsoredAdvertiser: "Wikipedia", sponsoredIabCategory: "5 - Education", - helpUrl: QuickSuggest.HELP_URL, - helpL10n: { - id: "urlbar-result-menu-learn-more-about-firefox-suggest", - }, isBlockable: true, blockL10n: { id: "urlbar-result-menu-dismiss-firefox-suggest", }, + isManageable: true, telemetryType: "adm_nonsponsored", }, }; diff --git a/browser/components/urlbar/tests/unit/test_l10nCache.js b/browser/components/urlbar/tests/unit/test_l10nCache.js index e92c75fa01..bd93cc50d6 100644 --- a/browser/components/urlbar/tests/unit/test_l10nCache.js +++ b/browser/components/urlbar/tests/unit/test_l10nCache.js @@ -1,7 +1,7 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -// Tests L10nCache in UrlbarUtils.jsm. +// Tests L10nCache in UrlbarUtils.sys.mjs. "use strict"; diff --git a/browser/config/version.txt b/browser/config/version.txt index 61eb5d32fe..ab2a50c778 100644 --- a/browser/config/version.txt +++ b/browser/config/version.txt @@ -1 +1 @@ -125.0.3 +126.0 diff --git a/browser/config/version_display.txt b/browser/config/version_display.txt index 61eb5d32fe..ab2a50c778 100644 --- a/browser/config/version_display.txt +++ b/browser/config/version_display.txt @@ -1 +1 @@ -125.0.3 +126.0 diff --git a/browser/docs/index.rst b/browser/docs/index.rst index cd4baa3141..2484af9882 100644 --- a/browser/docs/index.rst +++ b/browser/docs/index.rst @@ -31,4 +31,6 @@ This is the nascent documentation of the Firefox front-end code. components/storybook/docs/README.other-widgets.stories components/storybook/docs/README.lit-guide.stories components/storybook/docs/README.xul-and-html.stories + /toolkit/themes/shared/design-system/docs/README.design-tokens.stories + /toolkit/themes/shared/design-system/docs/README.json-design-tokens.stories components/backup/docs/index diff --git a/browser/extensions/formautofill/api.js b/browser/extensions/formautofill/api.js index 967b4a8d63..2733e1361f 100644 --- a/browser/extensions/formautofill/api.js +++ b/browser/extensions/formautofill/api.js @@ -48,14 +48,6 @@ function ensureCssLoaded(domWindow) { } insertStyleSheet(domWindow, "chrome://formautofill/content/formautofill.css"); - insertStyleSheet( - domWindow, - "chrome://formautofill/content/skin/autocomplete-item-shared.css" - ); - insertStyleSheet( - domWindow, - "chrome://formautofill/content/skin/autocomplete-item.css" - ); } this.formautofill = class extends ExtensionAPI { diff --git a/browser/extensions/formautofill/content/customElements.js b/browser/extensions/formautofill/content/customElements.js deleted file mode 100644 index 2f22a8173a..0000000000 --- a/browser/extensions/formautofill/content/customElements.js +++ /dev/null @@ -1,392 +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/. */ - -// This file is loaded into the browser window scope. -/* eslint-env mozilla/browser-window */ -/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded. - -"use strict"; - -// Wrap in a block to prevent leaking to window scope. -(() => { - function sendMessageToBrowser(msgName, data) { - let { AutoCompleteParent } = ChromeUtils.importESModule( - "resource://gre/actors/AutoCompleteParent.sys.mjs" - ); - - let actor = AutoCompleteParent.getCurrentActor(); - if (!actor) { - return; - } - - actor.manager.getActor("FormAutofill").sendAsyncMessage(msgName, data); - } - - class MozAutocompleteProfileListitemBase extends MozElements.MozRichlistitem { - constructor() { - super(); - - /** - * For form autofill, we want to unify the selection no matter by - * keyboard navigation or mouseover in order not to confuse user which - * profile preview is being shown. This field is set to true to indicate - * that selectedIndex of popup should be changed while mouseover item - */ - this.selectedByMouseOver = true; - } - - get _stringBundle() { - if (!this.__stringBundle) { - this.__stringBundle = Services.strings.createBundle( - "chrome://formautofill/locale/formautofill.properties" - ); - } - return this.__stringBundle; - } - - _cleanup() { - this.removeAttribute("formautofillattached"); - if (this._itemBox) { - this._itemBox.removeAttribute("size"); - } - } - - _onOverflow() {} - - _onUnderflow() {} - - handleOverUnderflow() {} - - _adjustAutofillItemLayout() { - let outerBoxRect = this.parentNode.getBoundingClientRect(); - - // Make item fit in popup as XUL box could not constrain - // item's width - this._itemBox.style.width = outerBoxRect.width + "px"; - // Use two-lines layout when width is smaller than 150px or - // 185px if an image precedes the label. - let oneLineMinRequiredWidth = this.getAttribute("ac-image") ? 185 : 150; - - if (outerBoxRect.width <= oneLineMinRequiredWidth) { - this._itemBox.setAttribute("size", "small"); - } else { - this._itemBox.removeAttribute("size"); - } - } - } - - MozElements.MozAutocompleteProfileListitem = class MozAutocompleteProfileListitem extends ( - MozAutocompleteProfileListitemBase - ) { - static get markup() { - return ` - <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box"> - <div class="profile-label-col profile-item-col"> - <span class="profile-label"></span> - </div> - <div class="profile-comment-col profile-item-col"> - <span class="profile-comment"></span> - </div> - </div> - `; - } - - connectedCallback() { - if (this.delayConnectedCallback()) { - return; - } - - this.textContent = ""; - - this.appendChild(this.constructor.fragment); - - this._itemBox = this.querySelector(".autofill-item-box"); - this._label = this.querySelector(".profile-label"); - this._comment = this.querySelector(".profile-comment"); - - this.initializeAttributeInheritance(); - this._adjustAcItem(); - } - - static get inheritedAttributes() { - return { - ".autofill-item-box": "ac-image", - }; - } - - set selected(val) { - if (val) { - this.setAttribute("selected", "true"); - } else { - this.removeAttribute("selected"); - } - - sendMessageToBrowser("FormAutofill:PreviewProfile"); - } - - get selected() { - return this.getAttribute("selected") == "true"; - } - - _adjustAcItem() { - this._adjustAutofillItemLayout(); - this.setAttribute("formautofillattached", "true"); - this._itemBox.style.setProperty( - "--primary-icon", - `url(${this.getAttribute("ac-image")})` - ); - - let { primary, secondary, ariaLabel } = JSON.parse( - this.getAttribute("ac-value") - ); - - this._label.textContent = primary.toString().replaceAll("*", "•"); - this._comment.textContent = secondary.toString().replaceAll("*", "•"); - if (ariaLabel) { - this.setAttribute("aria-label", ariaLabel); - } - } - }; - - customElements.define( - "autocomplete-profile-listitem", - MozElements.MozAutocompleteProfileListitem, - { extends: "richlistitem" } - ); - - class MozAutocompleteProfileListitemFooter extends MozAutocompleteProfileListitemBase { - static get markup() { - return ` - <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box autofill-footer"> - <div class="autofill-footer-row autofill-warning"></div> - <div class="autofill-footer-row autofill-button"></div> - </div> - `; - } - - constructor() { - super(); - - this.addEventListener("click", event => { - if (event.button != 0) { - return; - } - - if (this._warningTextBox.contains(event.originalTarget)) { - return; - } - - window.openPreferences("privacy-form-autofill"); - }); - } - - connectedCallback() { - if (this.delayConnectedCallback()) { - return; - } - - this.textContent = ""; - this.appendChild(this.constructor.fragment); - - this._itemBox = this.querySelector(".autofill-footer"); - this._optionButton = this.querySelector(".autofill-button"); - this._warningTextBox = this.querySelector(".autofill-warning"); - - /** - * A handler for updating warning message once selectedIndex has been changed. - * - * There're three different states of warning message: - * 1. None of addresses were selected: We show all the categories intersection of fields in the - * form and fields in the results. - * 2. An address was selested: Show the additional categories that will also be filled. - * 3. An address was selected, but the focused category is the same as the only one category: Only show - * the exact category that we're going to fill in. - * - * @private - * @param {object} data - * Message data - * @param {string[]} data.categories - * The categories of all the fields contained in the selected address. - */ - this.updateWarningNote = data => { - let categories = - data && data.categories ? data.categories : this._allFieldCategories; - // If the length of categories is 1, that means all the fillable fields are in the same - // category. We will change the way to inform user according to this flag. When the value - // is true, we show "Also autofills ...", otherwise, show "Autofills ..." only. - let hasExtraCategories = categories.length > 1; - // Show the categories in certain order to conform with the spec. - let orderedCategoryList = [ - { id: "address", l10nId: "category.address" }, - { id: "name", l10nId: "category.name" }, - { id: "organization", l10nId: "category.organization2" }, - { id: "tel", l10nId: "category.tel" }, - { id: "email", l10nId: "category.email" }, - ]; - let showCategories = hasExtraCategories - ? orderedCategoryList.filter( - category => - categories.includes(category.id) && - category.id != this._focusedCategory - ) - : [ - orderedCategoryList.find( - category => category.id == this._focusedCategory - ), - ]; - - let separator = - this._stringBundle.GetStringFromName("fieldNameSeparator"); - let warningTextTmplKey = hasExtraCategories - ? "phishingWarningMessage" - : "phishingWarningMessage2"; - let categoriesText = showCategories - .map(category => - this._stringBundle.GetStringFromName(category.l10nId) - ) - .join(separator); - - this._warningTextBox.textContent = - this._stringBundle.formatStringFromName(warningTextTmplKey, [ - categoriesText, - ]); - this.parentNode.parentNode.adjustHeight(); - }; - - this._adjustAcItem(); - } - - _onCollapse() { - if (this.showWarningText) { - let { FormAutofillParent } = ChromeUtils.importESModule( - "resource://autofill/FormAutofillParent.sys.mjs" - ); - FormAutofillParent.removeMessageObserver(this); - } - this._itemBox.removeAttribute("no-warning"); - } - - _adjustAcItem() { - this._adjustAutofillItemLayout(); - this.setAttribute("formautofillattached", "true"); - - let value = JSON.parse(this.getAttribute("ac-value")); - - this._allFieldCategories = value.categories; - this._focusedCategory = value.focusedCategory; - this.showWarningText = this._allFieldCategories && this._focusedCategory; - - if (this.showWarningText) { - let { FormAutofillParent } = ChromeUtils.importESModule( - "resource://autofill/FormAutofillParent.sys.mjs" - ); - FormAutofillParent.addMessageObserver(this); - this.updateWarningNote(); - } else { - this._itemBox.setAttribute("no-warning", "true"); - } - - this._optionButton.textContent = value.manageLabel; - } - } - - customElements.define( - "autocomplete-profile-listitem-footer", - MozAutocompleteProfileListitemFooter, - { extends: "richlistitem" } - ); - - class MozAutocompleteCreditcardInsecureField extends MozAutocompleteProfileListitemBase { - static get markup() { - return ` - <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-insecure-item"></div> - `; - } - - connectedCallback() { - if (this.delayConnectedCallback()) { - return; - } - this.textContent = ""; - this.appendChild(this.constructor.fragment); - - this._itemBox = this.querySelector(".autofill-insecure-item"); - - this._adjustAcItem(); - } - - set selected(val) { - // This item is unselectable since we see this item as a pure message. - } - - get selected() { - return this.getAttribute("selected") == "true"; - } - - _adjustAcItem() { - this._adjustAutofillItemLayout(); - this.setAttribute("formautofillattached", "true"); - - let value = this.getAttribute("ac-value"); - this._itemBox.textContent = value; - } - } - - customElements.define( - "autocomplete-creditcard-insecure-field", - MozAutocompleteCreditcardInsecureField, - { extends: "richlistitem" } - ); - - class MozAutocompleteProfileListitemClearButton extends MozAutocompleteProfileListitemBase { - static get markup() { - return ` - <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box autofill-footer"> - <div class="autofill-footer-row autofill-button"></div> - </div> - `; - } - - constructor() { - super(); - - this.addEventListener("click", event => { - if (event.button != 0) { - return; - } - - sendMessageToBrowser("FormAutofill:ClearForm"); - }); - } - - connectedCallback() { - if (this.delayConnectedCallback()) { - return; - } - - this.textContent = ""; - this.appendChild(this.constructor.fragment); - - this._itemBox = this.querySelector(".autofill-item-box"); - this._clearBtn = this.querySelector(".autofill-button"); - - this._adjustAcItem(); - } - - _adjustAcItem() { - this._adjustAutofillItemLayout(); - this.setAttribute("formautofillattached", "true"); - - let clearFormBtnLabel = - this._stringBundle.GetStringFromName("clearFormBtnLabel2"); - this._clearBtn.textContent = clearFormBtnLabel; - } - } - - customElements.define( - "autocomplete-profile-listitem-clear-button", - MozAutocompleteProfileListitemClearButton, - { extends: "richlistitem" } - ); -})(); diff --git a/browser/extensions/formautofill/content/formautofill.css b/browser/extensions/formautofill/content/formautofill.css index 911b152f8d..8cf13da601 100644 --- a/browser/extensions/formautofill/content/formautofill.css +++ b/browser/extensions/formautofill/content/formautofill.css @@ -3,19 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #PopupAutoComplete { - &[resultstyles~="autofill-profile"] { + &[resultstyles~="autofill"] { min-width: 150px !important; } - &[resultstyles~="autofill-insecureWarning"] { - min-width: 200px !important; - } - > richlistbox > richlistitem { - &[originaltype="autofill-profile"], - &[originaltype="autofill-footer"], - &[originaltype="autofill-insecureWarning"], - &[originaltype="autofill-clear-button"] { + &[originaltype="autofill"] { display: block; margin: 0; padding: 0; diff --git a/browser/extensions/formautofill/locales/en-US/formautofill.properties b/browser/extensions/formautofill/locales/en-US/formautofill.properties index d3add192d7..f63dbf8e20 100644 --- a/browser/extensions/formautofill/locales/en-US/formautofill.properties +++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties @@ -2,27 +2,9 @@ # 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/. -# LOCALIZATION NOTE (category.address, category.name, category.organization2, category.tel, category.email): -# Used in autofill drop down suggestion to indicate what other categories Form Autofill will attempt to fill. -category.address = address -category.name = name -category.organization2 = organization -category.tel = phone -category.email = email -# LOCALIZATION NOTE (fieldNameSeparator): This is used as a separator between categories. -fieldNameSeparator = ,\u0020 -# LOCALIZATION NOTE (phishingWarningMessage, phishingWarningMessage2): The warning -# text that is displayed for informing users what categories are about to be filled. -# "%S" will be replaced with a list generated from the pre-defined categories. -# The text would be e.g. Also autofills organization, phone, email. -phishingWarningMessage = Also autofills %S -phishingWarningMessage2 = Autofills %S # LOCALIZATION NOTE (insecureFieldWarningDescription): %S is brandShortName. This string is used in drop down # suggestion when users try to autofill credit card on an insecure website (without https). insecureFieldWarningDescription = %S has detected an insecure site. Form Autofill is temporarily disabled. -# LOCALIZATION NOTE (clearFormBtnLabel2): Label for the button in the dropdown menu that used to clear the populated -# form. -clearFormBtnLabel2 = Clear Autofill Form learnMoreLabel = Learn more # LOCALIZATION NOTE (savedAddressesBtnLabel): Label for the button that opens a dialog that shows the diff --git a/browser/extensions/formautofill/moz.build b/browser/extensions/formautofill/moz.build index 2a94a19341..4dd89dc6ab 100644 --- a/browser/extensions/formautofill/moz.build +++ b/browser/extensions/formautofill/moz.build @@ -18,17 +18,14 @@ FINAL_TARGET_FILES.features["formautofill@mozilla.org"] += [ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": FINAL_TARGET_FILES.features["formautofill@mozilla.org"].chrome.content.skin += [ - "skin/linux/autocomplete-item.css", "skin/linux/editDialog.css", ] elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": FINAL_TARGET_FILES.features["formautofill@mozilla.org"].chrome.content.skin += [ - "skin/osx/autocomplete-item.css", "skin/osx/editDialog.css", ] elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": FINAL_TARGET_FILES.features["formautofill@mozilla.org"].chrome.content.skin += [ - "skin/windows/autocomplete-item.css", "skin/windows/editDialog.css", ] diff --git a/browser/extensions/formautofill/skin/linux/autocomplete-item.css b/browser/extensions/formautofill/skin/linux/autocomplete-item.css deleted file mode 100644 index 8f782aaa2a..0000000000 --- a/browser/extensions/formautofill/skin/linux/autocomplete-item.css +++ /dev/null @@ -1,10 +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/. */ - -@namespace url("http://www.w3.org/1999/xhtml"); - - -.autofill-item-box { - --default-font-size: 14.25; -} diff --git a/browser/extensions/formautofill/skin/osx/autocomplete-item.css b/browser/extensions/formautofill/skin/osx/autocomplete-item.css deleted file mode 100644 index 121c1139da..0000000000 --- a/browser/extensions/formautofill/skin/osx/autocomplete-item.css +++ /dev/null @@ -1,18 +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/. */ - -@namespace url("http://www.w3.org/1999/xhtml"); - -/* On Mac, the autocomplete panel changes color in system dark mode. We need - to change the contrast on warning-background-color accordingly. */ -@media (prefers-color-scheme: dark) { - .autofill-item-box { - --warning-background-color: rgba(248,232,28,.6); - } - } - - -.autofill-item-box { - --default-font-size: 11; -} diff --git a/browser/extensions/formautofill/skin/shared/autocomplete-item-shared.css b/browser/extensions/formautofill/skin/shared/autocomplete-item-shared.css deleted file mode 100644 index 876b8d6651..0000000000 --- a/browser/extensions/formautofill/skin/shared/autocomplete-item-shared.css +++ /dev/null @@ -1,182 +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/. */ - -@namespace url("http://www.w3.org/1999/xhtml"); -@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); - - -xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .autofill-item-box { - background-color: SelectedItem; - color: SelectedItemText; -} - -xul|richlistitem[originaltype="autofill-footer"][selected="true"] > .autofill-item-box > .autofill-button, -xul|richlistitem[originaltype="autofill-clear-button"][selected="true"] > .autofill-item-box > .autofill-button { - background-color: ButtonHighlight; -} - -xul|richlistitem[originaltype="autofill-insecureWarning"] { - border-bottom: 1px solid var(--panel-separator-color); - background-color: var(--arrowpanel-dimmed); -} - -.autofill-item-box { - --item-padding-vertical: 7px; - --item-padding-horizontal: 10px; - --col-spacer: 7px; - --item-width: calc(50% - (var(--col-spacer) / 2)); - --comment-text-color: GreyText; - --warning-text-color: GreyText; - --warning-background-color: rgba(248, 232, 28, .2); - - --default-font-size: 12; - --label-font-size: 12; - --comment-font-size: 10; - --warning-font-size: 10; - --btn-font-size: 11; -} - -.autofill-item-box[size="small"] { - --item-padding-vertical: 7px; - --col-spacer: 0px; - --row-spacer: 3px; - --item-width: 100%; -} - -.autofill-item-box:not([ac-image=""]) { - --item-padding-vertical: 6.5px; - --comment-font-size: 11; -} - -.autofill-footer, -.autofill-footer[size="small"] { - --item-width: 100%; - --item-padding-vertical: 0; - --item-padding-horizontal: 0; -} - -.autofill-item-box { - box-sizing: border-box; - margin: 0; - border-bottom: 1px solid rgba(38,38,38,.15); - padding: var(--item-padding-vertical) 0; - padding-inline: var(--item-padding-horizontal); - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - background-color: Field; - color: FieldText; -} - -.autofill-item-box:last-child { - border-bottom: 0; -} - -.autofill-item-box > .profile-item-col { - box-sizing: border-box; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: var(--item-width); -} - -.autofill-item-box > .profile-label-col { - text-align: start; -} - -.autofill-item-box:not([ac-image=""]) > .profile-label-col::before { - margin-inline-end: 5px; - float: inline-start; - content: ""; - width: 16px; - height: 16px; - background-image: var(--primary-icon); - background-size: contain; - background-repeat: no-repeat; - background-position: center; - -moz-context-properties: fill; - fill: var(--comment-text-color) -} - -.autofill-item-box > .profile-label-col > .profile-label { - font-size: calc(var(--label-font-size) / var(--default-font-size) * 1em); - unicode-bidi: plaintext; -} - -.autofill-item-box > .profile-comment-col { - margin-inline-start: var(--col-spacer); - text-align: end; - color: var(--comment-text-color); -} - -.autofill-item-box > .profile-comment-col > .profile-comment { - font-size: calc(var(--comment-font-size) / var(--default-font-size) * 1em); - unicode-bidi: plaintext; -} - -.autofill-item-box[size="small"] { - flex-direction: column; -} - -.autofill-item-box[size="small"] > .profile-comment-col { - margin-top: var(--row-spacer); - text-align: start; -} - -.autofill-footer { - padding: 0; - flex-direction: column; -} - -.autofill-footer > .autofill-footer-row { - display: flex; - justify-content: center; - align-items: center; - width: var(--item-width); -} - -.autofill-footer > .autofill-warning { - padding: 2.5px 0; - color: var(--warning-text-color); - text-align: center; - background-color: var(--warning-background-color); - border-bottom: 1px solid rgba(38,38,38,.15); - font-size: calc(var(--warning-font-size) / var(--default-font-size) * 1em); -} - -.autofill-footer > .autofill-button { - box-sizing: border-box; - padding: 0 10px; - min-height: 40px; - background-color: ButtonFace; - font-size: calc(var(--btn-font-size) / var(--default-font-size) * 1em); - color: ButtonText; - text-align: center; -} - -.autofill-footer[no-warning="true"] > .autofill-warning { - display: none; -} - -.autofill-insecure-item { - box-sizing: border-box; - padding: 4px 0; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-items: center; - color: GrayText; -} - -.autofill-insecure-item::before { - display: block; - margin-inline: 4px 8px; - content: ""; - width: 16px; - height: 16px; - background-image: url(chrome://global/skin/icons/security-broken.svg); - -moz-context-properties: fill; - fill: GrayText; -} diff --git a/browser/extensions/formautofill/skin/windows/autocomplete-item.css b/browser/extensions/formautofill/skin/windows/autocomplete-item.css deleted file mode 100644 index 4f0cb71346..0000000000 --- a/browser/extensions/formautofill/skin/windows/autocomplete-item.css +++ /dev/null @@ -1,25 +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/. */ - -@namespace url("http://www.w3.org/1999/xhtml"); -@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); - -.autofill-item-box { - --default-font-size: 12; -} - -xul|richlistitem[originaltype="autofill-footer"][selected="true"] > .autofill-item-box > .autofill-button, -xul|richlistitem[originaltype="autofill-clear-button"][selected="true"] > .autofill-item-box > .autofill-button { - background-color: color-mix(in srgb, Field 90%, FieldText); -} - -@media (prefers-contrast) { - xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .autofill-item-box { - background-color: SelectedItem; - } - - .autofill-item-box { - --comment-text-color: GrayText; - } -} diff --git a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_state.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_state.js index f3b04d7f9c..003228c1cc 100644 --- a/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_state.js +++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_state.js @@ -95,6 +95,10 @@ add_task(async function test_save_doorhanger_state_valid() { expected: { "address-level1": "CA" }, }, { + filled: { "address-level1": "CA-BC" }, + expected: { "address-level1": "CA-BC" }, + }, + { filled: { "address-level1": "california" }, expected: { "address-level1": "california" }, }, diff --git a/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display_state.js b/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display_state.js index 1d8933ad31..ccddbc743d 100644 --- a/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display_state.js +++ b/browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display_state.js @@ -40,6 +40,10 @@ add_task(async function test_edit_doorhanger_display_state() { filled: { "address-level1": "Washington" }, expected: { label: "WA" }, }, + { + filled: { "address-level1": "CA-BC", country: "CA" }, + expected: { label: "BC" }, + }, ]; for (const TEST of TEST_CASES) { @@ -54,6 +58,7 @@ add_task(async function test_edit_doorhanger_display_state() { "#organization": DEFAULT.organization, "#street-address": DEFAULT["street-address"], "#address-level1": TEST.filled["address-level1"], + "#country": TEST.filled.country || DEFAULT.country, }, }); await onSavePopupShown; diff --git a/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js b/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js index 1e7ba523e8..1b4c934a38 100644 --- a/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js +++ b/browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js @@ -14,6 +14,15 @@ add_setup(async function setup_storage() { ); }); +function getFooterLabel(itemsBox) { + let footer = itemsBox.getItemAtIndex(itemsBox.itemCount - 1); + while (footer.collapsed) { + footer = footer.previousSibling; + } + + return footer.querySelector(".line1-label"); +} + add_task(async function test_footer_has_correct_button_text_on_address() { await BrowserTestUtils.withNewTab( { gBrowser, url: URL }, @@ -23,9 +32,7 @@ add_task(async function test_footer_has_correct_button_text_on_address() { } = browser; await openPopupOn(browser, "#organization"); - const footer = itemsBox.querySelector( - ".autofill-footer-row.autofill-button" - ); + let footer = getFooterLabel(itemsBox); Assert.equal( footer.innerText, l10n.formatValueSync("autofill-manage-addresses-label") @@ -44,9 +51,7 @@ add_task(async function test_footer_has_correct_button_text_on_credit_card() { } = browser; await openPopupOn(browser, "#cc-number"); - const footer = itemsBox.querySelector( - ".autofill-footer-row.autofill-button" - ); + let footer = getFooterLabel(itemsBox); Assert.equal( footer.innerText, l10n.formatValueSync("autofill-manage-payment-methods-label") @@ -65,6 +70,7 @@ add_task(async function test_press_enter_on_footer() { } = browser; await openPopupOn(browser, "#organization"); + // Navigate to the footer and press enter. const listItemElems = itemsBox.querySelectorAll( ".autocomplete-richlistitem" @@ -75,7 +81,7 @@ add_task(async function test_press_enter_on_footer() { true ); for (let i = 0; i < listItemElems.length; i++) { - if (!listItemElems[i].collapsed) { + if (!listItemElems[i].disabled) { await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); } } @@ -110,7 +116,6 @@ add_task(async function test_click_on_footer() { while (optionButton.collapsed) { optionButton = optionButton.previousElementSibling; } - optionButton = optionButton._optionButton; const prefTabPromise = BrowserTestUtils.waitForNewTab( gBrowser, @@ -140,15 +145,7 @@ add_task(async function test_phishing_warning_single_category() { await BrowserTestUtils.withNewTab( { gBrowser, url: URL }, async function (browser) { - const { - autoCompletePopup: { richlistbox: itemsBox }, - } = browser; - await openPopupOn(browser, "#tel"); - const warningBox = itemsBox.querySelector( - ".autocomplete-richlistitem:last-child" - )._warningTextBox; - ok(warningBox, "Got phishing warning box"); await expectWarningText(browser, "Also autofills address"); await closePopup(browser); } diff --git a/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js b/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js index bc1d2fccab..41d57c20df 100644 --- a/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js +++ b/browser/extensions/formautofill/test/browser/browser_dropdown_layout.js @@ -7,22 +7,6 @@ add_task(async function setup_storage() { await setStorage(TEST_ADDRESS_1, TEST_ADDRESS_2, TEST_ADDRESS_3); }); -async function reopenPopupWithResizedInput(browser, selector, newSize) { - await closePopup(browser); - /* eslint no-shadow: ["error", { "allow": ["selector", "newSize"] }] */ - await SpecialPowers.spawn( - browser, - [{ selector, newSize }], - async function ({ selector, newSize }) { - const input = content.document.querySelector(selector); - - input.style.boxSizing = "border-box"; - input.style.width = newSize + "px"; - } - ); - await openPopupOn(browser, selector); -} - add_task(async function test_address_dropdown() { await BrowserTestUtils.withNewTab( { gBrowser, url: URL }, @@ -33,20 +17,6 @@ add_task(async function test_address_dropdown() { is(firstItem.getAttribute("ac-image"), "", "Should not show icon"); - // The breakpoint of two-lines layout is 150px - await reopenPopupWithResizedInput(browser, focusInput, 140); - is( - firstItem._itemBox.getAttribute("size"), - "small", - "Show two-lines layout" - ); - await reopenPopupWithResizedInput(browser, focusInput, 160); - is( - firstItem._itemBox.hasAttribute("size"), - false, - "Show one-line layout" - ); - await closePopup(browser); } ); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js index 2b1fb9043c..ad28d857ae 100644 --- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js @@ -79,7 +79,7 @@ add_task(async function test_credit_card_dropdown_icon_invalid_types_select() { const creditCardItems = getDisplayedPopupItems( browser, - "[originaltype='autofill-profile']" + "[originaltype='autofill']" ); for (const [index, creditCardItem] of creditCardItems.entries()) { diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js b/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js index 5de499b942..09c2f7e195 100644 --- a/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js @@ -55,28 +55,28 @@ add_task(async function test_insecure_form() { urlPath: TEST_URL_PATH, protocol: "https", focusInput: "#organization", - expectedType: "autofill-profile", - expectedResultLength: 2, + expectedType: "autofill", + expectedResultLength: 3, // add one for the status row }, { urlPath: TEST_URL_PATH, protocol: "http", focusInput: "#organization", - expectedType: "autofill-profile", - expectedResultLength: 2, + expectedType: "autofill", + expectedResultLength: 3, // add one for the status row }, { urlPath: TEST_URL_PATH_CC, protocol: "https", focusInput: "#cc-name", - expectedType: "autofill-profile", - expectedResultLength: 3, + expectedType: "autofill", + expectedResultLength: 3, // no status row here }, { urlPath: TEST_URL_PATH_CC, protocol: "http", focusInput: "#cc-name", - expectedType: "autofill-insecureWarning", // insecure warning field + expectedType: "insecureWarning", // insecure warning field expectedResultLength: 1, }, ]; diff --git a/browser/extensions/formautofill/test/browser/head.js b/browser/extensions/formautofill/test/browser/head.js index 8de8488f1f..3f87f7b5ef 100644 --- a/browser/extensions/formautofill/test/browser/head.js +++ b/browser/extensions/formautofill/test/browser/head.js @@ -535,25 +535,6 @@ async function runAndWaitForAutocompletePopupOpen(browser, taskFn) { await taskFn(); await popupShown; - await BrowserTestUtils.waitForMutationCondition( - browser.autoCompletePopup.richlistbox, - { childList: true, subtree: true, attributes: true }, - () => { - const listItemElems = getDisplayedPopupItems(browser); - return ( - !![...listItemElems].length && - [...listItemElems].every(item => { - return ( - (item.getAttribute("originaltype") == "autofill-profile" || - item.getAttribute("originaltype") == "autofill-insecureWarning" || - item.getAttribute("originaltype") == "autofill-clear-button" || - item.getAttribute("originaltype") == "autofill-footer") && - item.hasAttribute("formautofillattached") - ); - }) - ); - } - ); } async function waitForPopupEnabled(browser) { @@ -595,7 +576,7 @@ function waitPopupStateInChild(bc, messageName) { async function openPopupOn(browser, selector) { let childNotifiedPromise = waitPopupStateInChild( browser, - "FormAutoComplete:PopupOpened" + "AutoComplete:PopupOpened" ); await SimpleTest.promiseFocus(browser); @@ -613,7 +594,7 @@ async function openPopupOn(browser, selector) { async function openPopupOnSubframe(browser, frameBrowsingContext, selector) { let childNotifiedPromise = waitPopupStateInChild( frameBrowsingContext, - "FormAutoComplete:PopupOpened" + "AutoComplete:PopupOpened" ); await SimpleTest.promiseFocus(browser); @@ -637,7 +618,7 @@ async function closePopup(browser) { let childNotifiedPromise = waitPopupStateInChild( browser, - "FormAutoComplete:PopupClosed" + "AutoComplete:PopupClosed" ); let popupClosePromise = BrowserTestUtils.waitForPopupEvent( browser.autoCompletePopup, @@ -655,7 +636,7 @@ async function closePopup(browser) { async function closePopupForSubframe(browser, frameBrowsingContext) { let childNotifiedPromise = waitPopupStateInChild( browser, - "FormAutoComplete:PopupClosed" + "AutoComplete:PopupClosed" ); let popupClosePromise = BrowserTestUtils.waitForPopupEvent( @@ -850,14 +831,8 @@ async function expectWarningText(browser, expectedText) { const { autoCompletePopup: { richlistbox: itemsBox }, } = browser; - let warningBox = itemsBox.querySelector( - ".autocomplete-richlistitem:last-child" - ); - - while (warningBox.collapsed) { - warningBox = warningBox.previousSibling; - } - warningBox = warningBox._warningTextBox; + let warningBox = itemsBox.querySelector(".ac-status"); + ok(warningBox.parentNode.disabled, "Got warning box and is disabled"); await BrowserTestUtils.waitForMutationCondition( warningBox, diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html index 8d1333b727..717d40946f 100644 --- a/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html +++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html @@ -59,6 +59,11 @@ async function setupFormHistory() { ]); } +function replaceStars(str) +{ + return str.replaceAll("*", "•") +} + initPopupListener(); // Form with history only. @@ -86,7 +91,7 @@ add_task(async function all_saved_fields_less_than_threshold() { synthesizeKey("KEY_ArrowDown"); checkMenuEntries([reducedMockRecord].map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ primary: cc["cc-name"], - secondary: cc.ccNumberFmt, + secondary: replaceStars(cc.ccNumberFmt), ariaLabel: `Visa ${cc["cc-name"]} ${cc.ccNumberFmt}`, image: expected.image, }))); @@ -102,8 +107,8 @@ add_task(async function check_menu_when_both_existed() { await expectPopup(); synthesizeKey("KEY_ArrowDown"); checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ - primary: cc.ccNumberFmt, - secondary: cc["cc-name"], + primary: replaceStars(cc.ccNumberFmt), + secondary: cc["cc-name"].toString(), ariaLabel: `${getCCTypeName(cc)} ${cc.ccNumberFmt.replaceAll("*", "")} ${cc["cc-name"]}`, image: expected.image, }))); @@ -112,8 +117,8 @@ add_task(async function check_menu_when_both_existed() { await expectPopup(); synthesizeKey("KEY_ArrowDown"); checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ - primary: cc["cc-name"], - secondary: cc.ccNumberFmt, + primary: cc["cc-name"].toString(), + secondary: replaceStars(cc.ccNumberFmt), ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt}`, image: expected.image, }))); @@ -122,8 +127,8 @@ add_task(async function check_menu_when_both_existed() { await expectPopup(); synthesizeKey("KEY_ArrowDown"); checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ - primary: cc["cc-exp-year"], - secondary: cc.ccNumberFmt, + primary: cc["cc-exp-year"].toString(), + secondary: replaceStars(cc.ccNumberFmt), ariaLabel: `${getCCTypeName(cc)} ${cc["cc-exp-year"]} ${cc.ccNumberFmt}`, image: expected.image, }))); @@ -132,8 +137,8 @@ add_task(async function check_menu_when_both_existed() { await expectPopup(); synthesizeKey("KEY_ArrowDown"); checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ - primary: cc["cc-exp-month"], - secondary: cc.ccNumberFmt, + primary: cc["cc-exp-month"].toString(), + secondary: replaceStars(cc.ccNumberFmt), ariaLabel: `${getCCTypeName(cc)} ${cc["cc-exp-month"]} ${cc.ccNumberFmt}`, image: expected.image, }))); @@ -185,8 +190,8 @@ add_task(async function check_fields_after_form_autofill() { // The popup doesn't auto-show on focus because the field isn't empty await expectPopup(); checkMenuEntries(MOCK_STORAGE.slice(1).map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ - primary: cc["cc-exp-year"], - secondary: cc.ccNumberFmt, + primary: cc["cc-exp-year"].toString(), + secondary: replaceStars(cc.ccNumberFmt), ariaLabel: `${getCCTypeName(cc)} ${cc["cc-exp-year"]} ${cc.ccNumberFmt}`, image: expected.image, }))); @@ -220,7 +225,7 @@ add_task(async function check_cc_popup_on_field_blank() { await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ primary: cc["cc-name"], - secondary: cc.ccNumberFmt, + secondary: replaceStars(cc.ccNumberFmt), ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt}`, image: expected.image, }))); @@ -240,7 +245,7 @@ add_task(async function check_form_autofill_resume() { await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ primary: cc["cc-name"], - secondary: cc.ccNumberFmt, + secondary: replaceStars(cc.ccNumberFmt), ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt}`, image: expected.image, }))); diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html index 04ff6ff85c..c42a1ad2d0 100644 --- a/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html +++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html @@ -49,6 +49,11 @@ async function setupFormHistory() { ]); } +function replaceStars(str) +{ + return str.replaceAll("*", "•") +} + initPopupListener(); // Show Form History popup for non-autocomplete="off" field only @@ -73,7 +78,7 @@ add_task(async function check_menu_when_both_with_autocomplete_off() { synthesizeKey("KEY_ArrowDown"); await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ - primary: cc.ccNumberFmt, + primary: replaceStars(cc.ccNumberFmt), secondary: cc["cc-name"], ariaLabel: `${getCCTypeName(cc)} ${cc.ccNumberFmt.replaceAll("*", "")} ${cc["cc-name"]}`, image: expected.image, @@ -84,7 +89,7 @@ add_task(async function check_menu_when_both_with_autocomplete_off() { await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(patchRecordCCNumber).map(({ cc, expected }) => JSON.stringify({ primary: cc["cc-name"], - secondary: cc.ccNumberFmt, + secondary: replaceStars(cc.ccNumberFmt), ariaLabel: `${getCCTypeName(cc)} ${cc["cc-name"]} ${cc.ccNumberFmt}`, image: expected.image, }))); diff --git a/browser/extensions/formautofill/test/mochitest/formautofill_common.js b/browser/extensions/formautofill/test/mochitest/formautofill_common.js index 6cdf9ca86b..0e371ba3af 100644 --- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js +++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js @@ -80,8 +80,9 @@ function clickOnElement(selector) { SimpleTest.executeSoon(() => element.click()); } -// The equivalent helper function to getAdaptedProfiles in FormAutofillHandler.jsm that -// transforms the given profile to expected filled profile. +// The equivalent helper function to getAdaptedProfiles in +// FormAutofillSection.sys.mjs that transforms the given profile to expected +// filled profile. function _getAdaptedProfile(profile) { const adaptedProfile = Object.assign({}, profile); @@ -270,12 +271,18 @@ async function onStorageChanged(type) { }); } -function checkMenuEntries(expectedValues, isFormAutofillResult = true) { +function makeAddressLabel({ primary, secondary, status }) { + return JSON.stringify({ + primary, + secondary, + status, + ariaLabel: primary + " " + secondary + " " + status, + }); +} + +function checkMenuEntries(expectedValues, extraRows = 1) { let actualValues = getMenuEntries(); - // Expect one more item would appear at the bottom as the footer if the result is from form autofill. - let expectedLength = isFormAutofillResult - ? expectedValues.length + 1 - : expectedValues.length; + let expectedLength = expectedValues.length + extraRows; is(actualValues.length, expectedLength, " Checking length of expected menu"); for (let i = 0; i < expectedValues.length; i++) { diff --git a/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html b/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html index ab3c08e89a..f3213d3708 100644 --- a/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html +++ b/browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html @@ -31,6 +31,8 @@ let MOCK_STORAGE = [{ initPopupListener(); +let statusText = 'Also autofills address, name, organization'; + add_task(async function setupStorage() { await addAddress(MOCK_STORAGE[0]); @@ -46,9 +48,13 @@ add_task(async function check_switch_autofill_form_popup() { await expectPopup(); checkMenuEntries( [ - `{"primary":"+13453453456","secondary":"123 Sesame Street."}`, + makeAddressLabel({ + primary: "+13453453456", + secondary: "123 Sesame Street.", + status: statusText + }), + `{"primary":"","secondary":"","status":"${statusText}","style":"status"}`, ], - true ); await testMenuEntry(0, "!(el instanceof MozElements.MozAutocompleteRichlistitem)"); @@ -60,7 +66,7 @@ add_task(async function check_switch_oridnal_form_popup() { await setInput("#username", ""); synthesizeKey("KEY_ArrowDown"); await expectPopup(); - checkMenuEntries(["petya"], false); + checkMenuEntries(["petya"], 0); await testMenuEntry(0, "el instanceof MozElements.MozAutocompleteRichlistitem"); }); @@ -73,9 +79,13 @@ add_task(async function check_switch_autofill_form_popup_back() { await expectPopup(); checkMenuEntries( [ - `{"primary":"+13453453456","secondary":"123 Sesame Street."}`, + makeAddressLabel({ + primary: "+13453453456", + secondary: "123 Sesame Street.", + status: statusText + }), + `{"primary":"","secondary":"","status":"${statusText}","style":"status"}`, ], - true ); await testMenuEntry(0, "!(el instanceof MozElements.MozAutocompleteRichlistitem)"); diff --git a/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html b/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html index e2240474c8..2aa34f0c54 100644 --- a/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html +++ b/browser/extensions/formautofill/test/mochitest/test_autofocus_form.html @@ -39,8 +39,12 @@ add_task(async function check_autocomplete_on_autofocus_field() { synthesizeKey("KEY_ArrowDown"); await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(address => - JSON.stringify({primary: address.organization, secondary: address["street-address"]}) - )); + makeAddressLabel({ + primary: address.organization, + secondary: address["street-address"], + status: "Also autofills address, phone" + }) + ), 2); }); </script> diff --git a/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html index a642b2abca..b8a50c7d7c 100644 --- a/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html +++ b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html @@ -81,44 +81,48 @@ add_task(async function check_menu_when_both_existed() { synthesizeKey("KEY_ArrowDown"); await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(address => - JSON.stringify({ + makeAddressLabel({ primary: address.organization, secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]), + status: "Also autofills address, phone" }) - )); + ), 2); await setInput("#street-address", ""); await notExpectPopup(); synthesizeKey("KEY_ArrowDown"); await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(address => - JSON.stringify({ + makeAddressLabel({ primary: FormAutofillUtils.toOneLineAddress(address["street-address"]), secondary: address.organization, + status: "Also autofills organization, phone" }) - )); + ), 2); await setInput("#tel", ""); await notExpectPopup(); synthesizeKey("KEY_ArrowDown"); await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(address => - JSON.stringify({ + makeAddressLabel({ primary: address.tel, secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]), + status: "Also autofills address, organization" }) - )); + ), 2); await setInput("#address-line1", ""); await notExpectPopup(); synthesizeKey("KEY_ArrowDown"); await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(address => - JSON.stringify({ + makeAddressLabel({ primary: FormAutofillUtils.toOneLineAddress(address["street-address"]), secondary: address.organization, + status: "Also autofills organization, phone" }) - )); + ), 2); }); // Display history search result if no matched data in addresses. @@ -152,11 +156,12 @@ add_task(async function check_fields_after_form_autofill() { synthesizeKey("KEY_ArrowDown"); await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(address => - JSON.stringify({ + makeAddressLabel({ primary: address.organization, secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]), + status: "Also autofills address, phone" }) - ).slice(1)); + ).slice(1), 2); synthesizeKey("KEY_ArrowDown"); await triggerAutofillAndCheckProfile(MOCK_STORAGE[1]); synthesizeKey("KEY_Escape"); @@ -180,11 +185,12 @@ add_task(async function check_form_autofill_resume() { await setInput("#tel", ""); await triggerPopupAndHoverItem("#tel", 0); checkMenuEntries(MOCK_STORAGE.map(address => - JSON.stringify({ + makeAddressLabel({ primary: address.tel, secondary: FormAutofillUtils.toOneLineAddress(address["street-address"]), + status: "Also autofills address, organization" }) - )); + ), 2); await triggerAutofillAndCheckProfile(MOCK_STORAGE[0]); }); diff --git a/browser/extensions/formautofill/test/mochitest/test_form_changes.html b/browser/extensions/formautofill/test/mochitest/test_form_changes.html index 1bfc655328..dfe91a63e1 100644 --- a/browser/extensions/formautofill/test/mochitest/test_form_changes.html +++ b/browser/extensions/formautofill/test/mochitest/test_form_changes.html @@ -61,8 +61,12 @@ async function checkFormChangeHappened(formId) { await expectPopup(); synthesizeKey("KEY_ArrowDown"); checkMenuEntries(MOCK_STORAGE.map(address => - JSON.stringify({primary: address.tel, secondary: address.name}) - )); + makeAddressLabel({ + primary: address.tel, + secondary: address.name, + status: "Also autofills name, organization" + }) + ), 2); // Click the first entry of the autocomplete popup and make sure all fields are autofilled synthesizeKey("KEY_Enter"); @@ -76,8 +80,9 @@ async function checkFormChangeHappened(formId) { // Click on an autofilled field would show an autocomplete popup with "clear form" entry checkMenuEntries([ - JSON.stringify({primary: "", secondary: ""}), // Clear Autofill Form - ], true); + "Clear Autofill Form", // Clear Autofill Form + "Manage addresses" // FormAutofill Preferemce + ], 0); // This is for checking the changes of element removed and added then. document.querySelector(`#${formId} input[name=address-level2]`).remove(); @@ -87,8 +92,12 @@ async function checkFormChangeHappened(formId) { synthesizeKey("KEY_ArrowDown"); await expectPopup(); checkMenuEntries(MOCK_STORAGE.map(address => - JSON.stringify({primary: address["address-level2"], secondary: address.name}) - )); + makeAddressLabel({ + primary: address["address-level2"], + secondary: address.name, + status: "Also autofills name, organization, phone" + }) + ), 2); // Make sure everything is autofilled in the end synthesizeKey("KEY_ArrowDown"); diff --git a/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html b/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html index 3a372ae34e..19c7a82fe4 100644 --- a/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html +++ b/browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html @@ -81,7 +81,7 @@ add_task(async function check_preview() { // Navigate to the footer synthesizeKey("KEY_ArrowDown"); - await notifySelectedIndex(MOCK_STORAGE.length); + await notifySelectedIndex(MOCK_STORAGE.length + 1); // skip over the status row await checkFormFieldsStyle(null); synthesizeKey("KEY_ArrowDown"); diff --git a/browser/extensions/formautofill/test/unit/test_addressComponent_state.js b/browser/extensions/formautofill/test/unit/test_addressComponent_state.js index 41d83e78c9..4e4c390008 100644 --- a/browser/extensions/formautofill/test/unit/test_addressComponent_state.js +++ b/browser/extensions/formautofill/test/unit/test_addressComponent_state.js @@ -7,14 +7,26 @@ const VALID_TESTS = [ ["CA", true], ["CA.", true], ["CC", false], + + // change region to CA + { region: "CA" }, + ["BC", true], + ["British Columbia", true], + ["CA-BC", true], ]; const COMPARE_TESTS = [ ["California", "california", SAME], // case insensitive ["CA", "california", SAME], ["CA", "ca", SAME], + ["CA", "CA.", SAME], ["California", "New Jersey", DIFFERENT], ["New York", "New Jersey", DIFFERENT], + + // change region to CA + { region: "CA" }, + ["British Columbia", "BC", SAME], + ["CA-BC", "BC", SAME], ]; const TEST_FIELD_NAME = "address-level1"; diff --git a/browser/extensions/formautofill/test/unit/test_getRecords.js b/browser/extensions/formautofill/test/unit/test_getRecords.js index 9a7e5e6ac7..1ecbccab22 100644 --- a/browser/extensions/formautofill/test/unit/test_getRecords.js +++ b/browser/extensions/formautofill/test/unit/test_getRecords.js @@ -85,7 +85,7 @@ add_task(async function test_getRecords() { sinon.stub(collection, "getAll"); collection.getAll.returns(Promise.resolve(expectedResult)); } - await FormAutofillParent._getRecords({ collectionName }); + await FormAutofillParent.getRecords({ collectionName }); if (collection) { Assert.equal(collection.getAll.called, true); collection.getAll.restore(); @@ -105,7 +105,7 @@ add_task(async function test_getRecords_addresses() { description: "If the search string could match 1 address", filter: { collectionName: "addresses", - info: { fieldName: "street-address" }, + fieldName: "street-address", searchString: "Some", }, expectedResult: [TEST_ADDRESS_2], @@ -114,7 +114,7 @@ add_task(async function test_getRecords_addresses() { description: "If the search string could match multiple addresses", filter: { collectionName: "addresses", - info: { fieldName: "country" }, + fieldName: "country", searchString: "u", }, expectedResult: [TEST_ADDRESS_1, TEST_ADDRESS_2], @@ -123,7 +123,7 @@ add_task(async function test_getRecords_addresses() { description: "If the search string could not match any address", filter: { collectionName: "addresses", - info: { fieldName: "street-address" }, + fieldName: "street-address", searchString: "test", }, expectedResult: [], @@ -132,7 +132,7 @@ add_task(async function test_getRecords_addresses() { description: "If the search string is empty", filter: { collectionName: "addresses", - info: { fieldName: "street-address" }, + fieldName: "street-address", searchString: "", }, expectedResult: [TEST_ADDRESS_1, TEST_ADDRESS_2], @@ -142,7 +142,7 @@ add_task(async function test_getRecords_addresses() { "Check if the filtering logic is free from searching special chars", filter: { collectionName: "addresses", - info: { fieldName: "street-address" }, + fieldName: "street-address", searchString: ".*", }, expectedResult: [], @@ -152,7 +152,7 @@ add_task(async function test_getRecords_addresses() { "Prevent broken while searching the property that does not exist", filter: { collectionName: "addresses", - info: { fieldName: "tel" }, + fieldName: "tel", searchString: "1", }, expectedResult: [], @@ -161,7 +161,7 @@ add_task(async function test_getRecords_addresses() { for (let testCase of testCases) { info("Starting testcase: " + testCase.description); - let result = await FormAutofillParent._getRecords(testCase.filter); + let result = await FormAutofillParent.getRecords(testCase.filter); Assert.deepEqual(result, testCase.expectedResult); } }); @@ -195,7 +195,7 @@ add_task(async function test_getRecords_creditCards() { description: "If the search string could match multiple creditCards", filter: { collectionName: "creditCards", - info: { fieldName: "cc-name" }, + fieldName: "cc-name", searchString: "John", }, expectedResult: encryptedCCRecords, @@ -204,7 +204,7 @@ add_task(async function test_getRecords_creditCards() { description: "If the search string could not match any creditCard", filter: { collectionName: "creditCards", - info: { fieldName: "cc-name" }, + fieldName: "cc-name", searchString: "T", }, expectedResult: [], @@ -215,7 +215,7 @@ add_task(async function test_getRecords_creditCards() { "if the search string could match multiple creditCards", filter: { collectionName: "creditCards", - info: { fieldName: "cc-number" }, + fieldName: "cc-number", searchString: "4", }, expectedResult: encryptedCCRecords, @@ -224,7 +224,7 @@ add_task(async function test_getRecords_creditCards() { description: "If the search string could match 1 creditCard", filter: { collectionName: "creditCards", - info: { fieldName: "cc-name" }, + fieldName: "cc-name", searchString: "John Doe", }, mpEnabled: true, @@ -234,7 +234,7 @@ add_task(async function test_getRecords_creditCards() { description: "Return all creditCards if focused field is cc number", filter: { collectionName: "creditCards", - info: { fieldName: "cc-number" }, + fieldName: "cc-number", searchString: "411", }, mpEnabled: true, @@ -252,7 +252,7 @@ add_task(async function test_getRecords_creditCards() { token.reset(); token.initPassword("password"); } - let result = await FormAutofillParent._getRecords(testCase.filter); + let result = await FormAutofillParent.getRecords(testCase.filter); Assert.deepEqual(result, testCase.expectedResult); } }); diff --git a/browser/extensions/formautofill/test/unit/test_phoneNumber.js b/browser/extensions/formautofill/test/unit/test_phoneNumber.js index 133e54f6d7..b776bfd8b5 100644 --- a/browser/extensions/formautofill/test/unit/test_phoneNumber.js +++ b/browser/extensions/formautofill/test/unit/test_phoneNumber.js @@ -1,5 +1,5 @@ /** - * Tests PhoneNumber.jsm and PhoneNumberNormalizer.jsm. + * Tests PhoneNumber.sys.mjs and PhoneNumberNormalizer.sys.mjs. */ "use strict"; diff --git a/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js b/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js index 7200bc8975..30cdae3d8a 100644 --- a/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js +++ b/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js @@ -51,6 +51,15 @@ let allFieldNames = [ "tel", ]; +function makeAddressLabel({ primary, secondary, status }) { + return JSON.stringify({ + primary, + secondary, + status, + ariaLabel: primary + " " + secondary + " " + status, + }); +} + let addressTestCases = [ { description: "Focus on an `organization` field", @@ -65,21 +74,23 @@ let addressTestCases = [ items: [ { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[0]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "Sesame Street", secondary: "123 Sesame Street.", + status: "Also autofills address, name, phone", }), image: "", }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[1]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "Mozilla", secondary: "331 E. Evelyn Avenue", + status: "Also autofills address, name, phone", }), image: "", }, @@ -99,31 +110,34 @@ let addressTestCases = [ items: [ { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[0]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "1-345-345-3456.", secondary: "123 Sesame Street.", + status: "Also autofills address, name, organization", }), image: "", }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[1]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "1-650-903-0800", secondary: "331 E. Evelyn Avenue", + status: "Also autofills address, name, organization", }), image: "", }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[2]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "1-000-000-0000", secondary: "321, No Name St. 2nd line 3rd line", + status: "Also autofills address", }), image: "", }, @@ -143,31 +157,34 @@ let addressTestCases = [ items: [ { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[0]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "123 Sesame Street.", secondary: "Timothy Berners-Lee", + status: "Also autofills name, organization, phone", }), image: "", }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[1]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "331 E. Evelyn Avenue", secondary: "John Doe", + status: "Also autofills name, organization, phone", }), image: "", }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[2]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "321, No Name St. 2nd line 3rd line", secondary: "1-000-000-0000", + status: "Also autofills phone", }), image: "", }, @@ -187,31 +204,34 @@ let addressTestCases = [ items: [ { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[0]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "123 Sesame Street.", secondary: "Timothy Berners-Lee", + status: "Also autofills name, organization, phone", }), image: "", }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[1]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "331 E. Evelyn Avenue", secondary: "John Doe", + status: "Also autofills name, organization, phone", }), image: "", }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[2]), - label: JSON.stringify({ + label: makeAddressLabel({ primary: "321, No Name St.", secondary: "1-000-000-0000", + status: "Also autofills phone", }), image: "", }, @@ -287,11 +307,11 @@ let creditCardTestCases = [ items: [ { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[0]), label: JSON.stringify({ primary: "Timothy Berners-Lee", - secondary: "****6785", + secondary: "••••6785", ariaLabel: "Visa Timothy Berners-Lee ****6785", image: "chrome://formautofill/content/third-party/cc-logo-visa.svg", }), @@ -299,11 +319,11 @@ let creditCardTestCases = [ }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[1]), label: JSON.stringify({ primary: "John Doe", - secondary: "****1234", + secondary: "••••1234", ariaLabel: "American Express John Doe ****1234", image: "chrome://formautofill/content/third-party/cc-logo-amex.png", }), @@ -325,10 +345,10 @@ let creditCardTestCases = [ items: [ { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[0]), label: JSON.stringify({ - primary: "****6785", + primary: "••••6785", secondary: "Timothy Berners-Lee", ariaLabel: "Visa 6785 Timothy Berners-Lee", image: "chrome://formautofill/content/third-party/cc-logo-visa.svg", @@ -337,10 +357,10 @@ let creditCardTestCases = [ }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[1]), label: JSON.stringify({ - primary: "****1234", + primary: "••••1234", secondary: "John Doe", ariaLabel: "American Express 1234 John Doe", image: "chrome://formautofill/content/third-party/cc-logo-amex.png", @@ -349,10 +369,10 @@ let creditCardTestCases = [ }, { value: "", - style: "autofill-profile", + style: "autofill", comment: JSON.stringify(matchingProfiles[2]), label: JSON.stringify({ - primary: "****5678", + primary: "••••5678", secondary: "", ariaLabel: "5678", image: "chrome://formautofill/content/icon-credit-card-generic.svg", @@ -416,7 +436,14 @@ add_task(async function test_all_patterns() { let expectedItemLength = expectedValue.items.length; // If the last item shows up as a footer, we expect one more item // than expected. - if (actual.getStyleAt(actual.matchCount - 1) == "autofill-footer") { + if (actual.getStyleAt(actual.matchCount - 1) == "action") { + expectedItemLength++; + } + // Add one row for the status. + if ( + actual.matchCount > 2 && + actual.getStyleAt(actual.matchCount - 2) == "status" + ) { expectedItemLength++; } diff --git a/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.js b/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.js index 804f4b08d5..9b26a99e1c 100644 --- a/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.js +++ b/browser/extensions/report-site-issue/experimentalAPIs/helpMenu.js @@ -19,7 +19,7 @@ this.helpMenu = class extends ExtensionAPI { context, name: "helpMenu", register: fire => { - let observer = (subject, topic, data) => { + let observer = subject => { let nativeTab = subject.wrappedJSObject; let tab = tabManager.convert(nativeTab); fire.async(tab); diff --git a/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js b/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js index ef01f94a26..3d0aa79747 100644 --- a/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js +++ b/browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js @@ -244,7 +244,7 @@ add_task(async function test_framework_detection() { ); let tab2 = await clickToReportAndAwaitReportTabLoad(); - await SpecialPowers.spawn(tab2.linkedBrowser, [], async function (args) { + await SpecialPowers.spawn(tab2.linkedBrowser, [], async function () { let doc = content.document; let detailsParam = doc.getElementById("details").innerText; const details = JSON.parse(detailsParam); @@ -269,7 +269,7 @@ add_task(async function test_fastclick_detection() { ); let tab2 = await clickToReportAndAwaitReportTabLoad(); - await SpecialPowers.spawn(tab2.linkedBrowser, [], async function (args) { + await SpecialPowers.spawn(tab2.linkedBrowser, [], async function () { let doc = content.document; let detailsParam = doc.getElementById("details").innerText; const details = JSON.parse(detailsParam); @@ -292,7 +292,7 @@ add_task(async function test_framework_label() { ); let tab2 = await clickToReportAndAwaitReportTabLoad(); - await SpecialPowers.spawn(tab2.linkedBrowser, [], async function (args) { + await SpecialPowers.spawn(tab2.linkedBrowser, [], async function () { let doc = content.document; let labelParam = doc.getElementById("label").innerText; const label = JSON.parse(labelParam); diff --git a/browser/extensions/screenshots/background/main.js b/browser/extensions/screenshots/background/main.js index 594ba798ee..c39590799d 100644 --- a/browser/extensions/screenshots/background/main.js +++ b/browser/extensions/screenshots/background/main.js @@ -64,7 +64,7 @@ this.main = (function () { _startShotFlow(tab, "keyboard-shortcut"); }); - const _startShotFlow = (tab, inputType) => { + const _startShotFlow = tab => { if (!tab) { // Not in a page/tab context, ignore return; @@ -137,7 +137,7 @@ this.main = (function () { catcher.watchPromise(incrementCount(...args)); }); - communication.register("openShot", async (sender, { url, copied }) => { + communication.register("openShot", async (sender, { copied }) => { if (copied) { const id = crypto.randomUUID(); const [title, message] = await getStrings([ diff --git a/browser/extensions/screenshots/background/takeshot.js b/browser/extensions/screenshots/background/takeshot.js index cde7d9df70..e8be5d9a80 100644 --- a/browser/extensions/screenshots/background/takeshot.js +++ b/browser/extensions/screenshots/background/takeshot.js @@ -17,7 +17,7 @@ this.takeshot = (function () { } ); - communication.register("getZoomFactor", sender => { + communication.register("getZoomFactor", () => { return getZoomFactor(); }); @@ -54,7 +54,7 @@ this.takeshot = (function () { browser.tabs.captureTab(null, options).then(dataUrl => { const image = new Image(); image.src = dataUrl; - return new Promise((resolve, reject) => { + return new Promise(resolve => { image.onload = catcher.watchFunction(() => { const xScale = devicePixelRatio; const yScale = devicePixelRatio; diff --git a/browser/extensions/screenshots/blobConverters.js b/browser/extensions/screenshots/blobConverters.js index 4e727ff271..6376709984 100644 --- a/browser/extensions/screenshots/blobConverters.js +++ b/browser/extensions/screenshots/blobConverters.js @@ -24,7 +24,7 @@ this.blobConverters = (function () { }; exports.blobToArray = function (blob) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { const reader = new FileReader(); reader.addEventListener("loadend", function () { resolve(reader.result); @@ -34,7 +34,7 @@ this.blobConverters = (function () { }; exports.blobToDataUrl = function (blob) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { const reader = new FileReader(); reader.addEventListener("loadend", function () { resolve(reader.result); diff --git a/browser/extensions/screenshots/build/thumbnailGenerator.js b/browser/extensions/screenshots/build/thumbnailGenerator.js index c80ccb6bac..b193378a43 100644 --- a/browser/extensions/screenshots/build/thumbnailGenerator.js +++ b/browser/extensions/screenshots/build/thumbnailGenerator.js @@ -90,7 +90,7 @@ this.thumbnailGenerator = (function () { const thumbnailDimensions = getThumbnailDimensions(imageWidth, imageHeight); - return new Promise((resolve, reject) => { + return new Promise(resolve => { const thumbnailImage = new Image(); let srcWidth = imageWidth; let srcHeight = imageHeight; diff --git a/browser/extensions/screenshots/clipboard.js b/browser/extensions/screenshots/clipboard.js index d4dbc38a14..6c133e124a 100644 --- a/browser/extensions/screenshots/clipboard.js +++ b/browser/extensions/screenshots/clipboard.js @@ -10,7 +10,7 @@ this.clipboard = (function () { const exports = {}; exports.copy = function (text) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { const element = document.createElement("iframe"); element.src = browser.runtime.getURL("blank.html"); // We can't actually hide the iframe while copying, but we can make diff --git a/browser/extensions/screenshots/selector/ui.js b/browser/extensions/screenshots/selector/ui.js index c8433a8f84..9e54a1dfb2 100644 --- a/browser/extensions/screenshots/selector/ui.js +++ b/browser/extensions/screenshots/selector/ui.js @@ -85,7 +85,7 @@ this.ui = (function () { document: null, window: null, display(installHandlerOnDocument) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { if (!this.element) { this.element = initializeIframe(); this.element.id = "firefox-screenshots-selection-iframe"; @@ -240,7 +240,7 @@ this.ui = (function () { document: null, window: null, display(installHandlerOnDocument, standardOverlayCallbacks) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { if (!this.element) { this.element = initializeIframe(); this.element.id = "firefox-screenshots-preselection-iframe"; @@ -381,7 +381,7 @@ this.ui = (function () { document: null, window: null, display(installHandlerOnDocument, standardOverlayCallbacks) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { if (!this.element) { this.element = initializeIframe(); this.element.id = "firefox-screenshots-preview-iframe"; diff --git a/browser/extensions/screenshots/selector/uicontrol.js b/browser/extensions/screenshots/selector/uicontrol.js index b690281083..0fc44bd433 100644 --- a/browser/extensions/screenshots/selector/uicontrol.js +++ b/browser/extensions/screenshots/selector/uicontrol.js @@ -445,7 +445,7 @@ this.uicontrol = (function () { }, /** When we find an element, maybe there's one that's just a little bit better... */ - evenBetterElement(node, origRect) { + evenBetterElement(node) { let el = node.parentNode; const ELEMENT_NODE = document.ELEMENT_NODE; while (el && el.nodeType === ELEMENT_NODE) { @@ -541,7 +541,7 @@ this.uicontrol = (function () { } }, - mouseup(event) { + mouseup() { // If we don't get into "dragging" then we attempt an autoselect if (mouseupNoAutoselect) { sendEvent("cancel-selection", "selection-background-mousedown"); diff --git a/browser/extensions/screenshots/sitehelper.js b/browser/extensions/screenshots/sitehelper.js index 719e76dad2..e25f510070 100644 --- a/browser/extensions/screenshots/sitehelper.js +++ b/browser/extensions/screenshots/sitehelper.js @@ -30,7 +30,7 @@ this.sitehelper = (function () { registerListener( "delete-everything", - catcher.watchFunction(event => { + catcher.watchFunction(() => { // FIXME: reset some data in the add-on }, false) ); diff --git a/browser/extensions/webcompat/about-compat/aboutCompat.js b/browser/extensions/webcompat/about-compat/aboutCompat.js index edf467edeb..118806de87 100644 --- a/browser/extensions/webcompat/about-compat/aboutCompat.js +++ b/browser/extensions/webcompat/about-compat/aboutCompat.js @@ -14,7 +14,7 @@ const portToAddon = (function () { function connect() { port = browser.runtime.connect({ name: "AboutCompatTab" }); port.onMessage.addListener(onMessageFromAddon); - port.onDisconnect.addListener(e => { + port.onDisconnect.addListener(() => { port = undefined; }); } diff --git a/browser/extensions/webcompat/data/shims.js b/browser/extensions/webcompat/data/shims.js index f26ad96d04..ab7234c217 100644 --- a/browser/extensions/webcompat/data/shims.js +++ b/browser/extensions/webcompat/data/shims.js @@ -673,6 +673,8 @@ const AVAILABLE_SHIMS = [ ["*://teams.microsoft.com/*", "*://login.microsoftonline.com/*"], ["*://*.teams.microsoft.us/*", "*://login.microsoftonline.us/*"], ["*://www.msn.com/*", "*://login.microsoftonline.com/*"], + ["*://support.microsoft.com/*", "*://login.microsoftonline.com/*"], + ["*://answers.microsoft.com/*", "*://login.microsoftonline.com/*"], ], contentScripts: [ { @@ -682,6 +684,8 @@ const AVAILABLE_SHIMS = [ "*://teams.microsoft.com/*", "*://*.teams.microsoft.us/*", "*://www.msn.com/*", + "*://support.microsoft.com/*", + "*://answers.microsoft.com/*", ], runAt: "document_start", }, diff --git a/browser/extensions/webcompat/data/ua_overrides.js b/browser/extensions/webcompat/data/ua_overrides.js index 3645e962f0..22124c4f23 100644 --- a/browser/extensions/webcompat/data/ua_overrides.js +++ b/browser/extensions/webcompat/data/ua_overrides.js @@ -225,7 +225,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1574564", config: { matches: ["*://*.ceskatelevize.cz/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -302,7 +302,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1622063", config: { matches: ["*://wp1-ext.usps.gov/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -429,7 +429,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1719859", config: { matches: ["*://*.saxoinvestor.fr/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -546,7 +546,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1753461", config: { matches: ["*://serieson.naver.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36"; }, }, @@ -565,7 +565,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1771200", config: { matches: ["*://*.animalplanet.com/video/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -584,7 +584,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1779059", config: { matches: ["*://member-m.lazada.co.id/address/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -603,7 +603,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1778168", config: { matches: ["*://watch.antennaplus.gr/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA({ desktopOS: "nonLinux", }); @@ -623,7 +623,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1776897", config: { matches: ["*://www.edencast.fr/zoomcast*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -641,7 +641,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1784361", config: { matches: ["*://*.coldwellbankerhomes.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -678,7 +678,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1819702", config: { matches: ["*://*.feelgoodcontacts.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -694,7 +694,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1823966", config: { matches: ["*://*.elearning.dmv.ca.gov/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -710,7 +710,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://*.admissions.nid.edu/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -726,7 +726,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://*.bankmandiri.co.id/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -742,7 +742,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://*.frankfred.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -758,7 +758,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://mobile.onvue.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -774,7 +774,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://*.avizia.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -790,7 +790,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://www.yourtexasbenefits.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -806,7 +806,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://www.free4talk.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -822,7 +822,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://watch.indee.tv/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -838,7 +838,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://viewer-ebook.books.com.tw/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -854,7 +854,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://jelly.jd.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -870,7 +870,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://*.kt.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -886,7 +886,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://*.oirsa.org/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -902,7 +902,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1827678", config: { matches: ["*://onp.cloud.waterloo.ca/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -927,7 +927,7 @@ const AVAILABLE_UA_OVERRIDES = [ "*://*.yebocasino.co.za/*", // 88409 "*://*.yabbycasino.com/*", // 108025 ], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -943,7 +943,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1830821", config: { matches: ["*://*.webcartop.jp/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -959,7 +959,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1830821", config: { matches: ["*://enjoy.point.auone.jp/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -976,7 +976,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1836109", config: { matches: ["*://watch.tonton.com.my/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -993,7 +993,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1836112", config: { matches: ["*://www.capcut.cn/editor*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1078,7 +1078,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1836182", config: { matches: ["*://*.flatsatshadowglen.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1097,7 +1097,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1849018", config: { matches: ["*://*.carefirst.com/myaccount*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1115,7 +1115,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1850455", config: { matches: ["*://*.frontgate.com/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1134,7 +1134,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1855088", config: { matches: ["*://hrmis2.eghrmis.gov.my/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1152,7 +1152,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1855102", config: { matches: ["*://my.southerncross.co.nz/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1197,7 +1197,7 @@ const AVAILABLE_UA_OVERRIDES = [ "*://magazine.kruidvat.be/*", "*://folder.kruidvat.nl/*", ], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1215,7 +1215,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1864999", config: { matches: ["*://*.autotrader.ca/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1234,7 +1234,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1865000", config: { matches: ["*://*.bmo.com/main/personal/*/getting-started/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1252,7 +1252,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1865004", config: { matches: ["*://*.digimart.net/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1270,7 +1270,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1865007", config: { matches: ["*://*.circle.ms/*"], - uaTransformer: originalUA => { + uaTransformer: () => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, @@ -1288,7 +1288,7 @@ const AVAILABLE_UA_OVERRIDES = [ bug: "1884779", config: { matches: ["*://*.memurlar.net/*"], - uaTransformer: originalUA => { + uaTransformer: _originalUA => { return UAHelpers.getDeviceAppropriateChromeUA(); }, }, diff --git a/browser/extensions/webcompat/experiment-apis/appConstants.js b/browser/extensions/webcompat/experiment-apis/appConstants.js index 2869f299a4..13900b9890 100644 --- a/browser/extensions/webcompat/experiment-apis/appConstants.js +++ b/browser/extensions/webcompat/experiment-apis/appConstants.js @@ -7,7 +7,7 @@ /* global AppConstants, ExtensionAPI, XPCOMUtils */ this.appConstants = class extends ExtensionAPI { - getAPI(context) { + getAPI() { return { appConstants: { getReleaseBranch: () => { diff --git a/browser/extensions/webcompat/experiment-apis/systemManufacturer.js b/browser/extensions/webcompat/experiment-apis/systemManufacturer.js index b7dc68415c..033e49daaa 100644 --- a/browser/extensions/webcompat/experiment-apis/systemManufacturer.js +++ b/browser/extensions/webcompat/experiment-apis/systemManufacturer.js @@ -7,7 +7,7 @@ /* global ExtensionAPI, Services, XPCOMUtils */ this.systemManufacturer = class extends ExtensionAPI { - getAPI(context) { + getAPI() { return { systemManufacturer: { getManufacturer() { diff --git a/browser/extensions/webcompat/experiment-apis/trackingProtection.js b/browser/extensions/webcompat/experiment-apis/trackingProtection.js index 0f5d9a4233..22a8a4bbea 100644 --- a/browser/extensions/webcompat/experiment-apis/trackingProtection.js +++ b/browser/extensions/webcompat/experiment-apis/trackingProtection.js @@ -63,7 +63,7 @@ class Manager { "@mozilla.org/url-classifier/channel-classifier-service;1" ].getService(Ci.nsIChannelClassifierService); this._classifierObserver = {}; - this._classifierObserver.observe = (subject, topic, data) => { + this._classifierObserver.observe = (subject, topic) => { switch (topic) { case "http-on-stop-request": { const { channelId } = subject.QueryInterface(Ci.nsIIdentChannel); @@ -163,7 +163,7 @@ function updateDFPIStatus() { } this.trackingProtection = class extends ExtensionAPI { - onShutdown(isAppShutdown) { + onShutdown() { if (manager) { manager.stop(); } diff --git a/browser/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js b/browser/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js index 7383a4e567..6372cd58e0 100644 --- a/browser/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js +++ b/browser/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js @@ -31,5 +31,5 @@ Object.defineProperty(navigator.wrappedJSObject, "plugins", { get: exportFunction(function () { return pluginsArray; }, window), - set: exportFunction(function (val) {}, window), + set: exportFunction(function () {}, window), }); diff --git a/browser/extensions/webcompat/injections/js/bug1855014-eksiseyler.com.js b/browser/extensions/webcompat/injections/js/bug1855014-eksiseyler.com.js index 9c22c762a9..87f981f0f1 100644 --- a/browser/extensions/webcompat/injections/js/bug1855014-eksiseyler.com.js +++ b/browser/extensions/webcompat/injections/js/bug1855014-eksiseyler.com.js @@ -23,5 +23,5 @@ Object.defineProperty(window.wrappedJSObject, "loggingEnabled", { return false; }, window), - set: exportFunction(function (value = {}) {}, window), + set: exportFunction(function () {}, window), }); diff --git a/browser/extensions/webcompat/lib/custom_functions.js b/browser/extensions/webcompat/lib/custom_functions.js index 97603e0424..041a8c0041 100644 --- a/browser/extensions/webcompat/lib/custom_functions.js +++ b/browser/extensions/webcompat/lib/custom_functions.js @@ -27,7 +27,7 @@ const replaceStringInRequest = ( carryover = replaced.slice(-carryoverLength); }; - filter.onstop = event => { + filter.onstop = () => { if (carryover.length) { filter.write(encoder.encode(carryover)); } diff --git a/browser/extensions/webcompat/lib/shims.js b/browser/extensions/webcompat/lib/shims.js index fedb4c38e9..793d0eefb2 100644 --- a/browser/extensions/webcompat/lib/shims.js +++ b/browser/extensions/webcompat/lib/shims.js @@ -765,7 +765,7 @@ class Shims { }); } - async _onMessageFromShim(payload, sender, sendResponse) { + async _onMessageFromShim(payload, sender) { const { tab, frameId } = sender; const { id, url } = tab; const { shimId, message } = payload; diff --git a/browser/extensions/webcompat/lib/ua_overrides.js b/browser/extensions/webcompat/lib/ua_overrides.js index 2426293f3f..dd3bef2393 100644 --- a/browser/extensions/webcompat/lib/ua_overrides.js +++ b/browser/extensions/webcompat/lib/ua_overrides.js @@ -87,7 +87,7 @@ class UAOverrides { const listeners = { onBeforeSendHeaders: listener }; if (blocks) { - const blistener = details => { + const blistener = () => { return { cancel: true }; }; diff --git a/browser/extensions/webcompat/manifest.json b/browser/extensions/webcompat/manifest.json index f15b3bb938..683ea6c1ae 100644 --- a/browser/extensions/webcompat/manifest.json +++ b/browser/extensions/webcompat/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "Web Compatibility Interventions", "description": "Urgent post-release fixes for web compatibility.", - "version": "125.0.0", + "version": "125.1.0", "browser_specific_settings": { "gecko": { "id": "webcompat@mozilla.org", diff --git a/browser/extensions/webcompat/shims/eluminate.js b/browser/extensions/webcompat/shims/eluminate.js index 3fa65c048c..7d72ed8442 100644 --- a/browser/extensions/webcompat/shims/eluminate.js +++ b/browser/extensions/webcompat/shims/eluminate.js @@ -32,7 +32,7 @@ if (!window.CM_DDX) { invokeFunctionWhenAvailable: a => { a(); }, - gup: d => "", + gup: _d => "", privacy: { isDoNotTrackEnabled: () => false, setDoNotTrack: () => {}, diff --git a/browser/extensions/webcompat/shims/google-ima.js b/browser/extensions/webcompat/shims/google-ima.js index 1f5e56239d..c9caef2182 100644 --- a/browser/extensions/webcompat/shims/google-ima.js +++ b/browser/extensions/webcompat/shims/google-ima.js @@ -124,9 +124,9 @@ if (!window.google?.ima?.VERSION) { setPpid(p) { this.#p = p; } - setSessionId(s) {} - setVpaidAllowed(a) {} - setVpaidMode(m) {} + setSessionId(_s) {} + setVpaidAllowed(_a) {} + setVpaidMode(_m) {} } ImaSdkSettings.CompanionBackfillMode = { ALWAYS: "always", @@ -174,7 +174,7 @@ if (!window.google?.ima?.VERSION) { getVersion() { return VERSION; } - requestAds(r, c) { + requestAds(_r, _c) { // If autoplay is disabled and the page is trying to autoplay a tracking // ad, then IMA fails with an error, and the page is expected to request // ads again later when the user clicks to play. @@ -222,7 +222,7 @@ if (!window.google?.ima?.VERSION) { getVolume() { return this.#volume; } - init(w, h, m, e) {} + init(_w, _h, _m, _e) {} isCustomClickTrackingUsed() { return false; } @@ -231,7 +231,7 @@ if (!window.google?.ima?.VERSION) { } pause() {} requestNextAdBreak() {} - resize(w, h, m) {} + resize(_w, _h, _m) {} resume() {} setVolume(v) { this.#volume = v; @@ -259,7 +259,7 @@ if (!window.google?.ima?.VERSION) { }); } stop() {} - updateAdsRenderingSettings(s) {} + updateAdsRenderingSettings(_s) {} } class AdsRenderingSettings {} diff --git a/browser/extensions/webcompat/shims/rambler-authenticator.js b/browser/extensions/webcompat/shims/rambler-authenticator.js index 1fe074b660..77abd35ab1 100644 --- a/browser/extensions/webcompat/shims/rambler-authenticator.js +++ b/browser/extensions/webcompat/shims/rambler-authenticator.js @@ -46,7 +46,7 @@ if (!window.ramblerIdHelper) { })(); const ramblerIdHelper = { - getProfileInfo: (successCallback, errorCallback) => { + getProfileInfo: (successCallback, _errorCallback) => { successCallback({}); }, openAuth: () => { diff --git a/browser/extensions/webcompat/shims/webtrends.js b/browser/extensions/webcompat/shims/webtrends.js index c7ef0069da..34bcdb7285 100644 --- a/browser/extensions/webcompat/shims/webtrends.js +++ b/browser/extensions/webcompat/shims/webtrends.js @@ -26,7 +26,7 @@ if (!window.WebTrends) { return this; } DCSext = {}; - init(obj) { + init(_obj) { return this; } track() { diff --git a/browser/fxr/content/fxrui.js b/browser/fxr/content/fxrui.js index dd0f55767a..641f3cd126 100644 --- a/browser/fxr/content/fxrui.js +++ b/browser/fxr/content/fxrui.js @@ -98,7 +98,7 @@ function setupBrowser() { "nsIWebProgressListener", "nsISupportsWeakReference", ]), - onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + onLocationChange() { // When URL changes, update the URL in the URL bar and update // whether the back/forward buttons are enabled. urlInput.value = browser.currentURI.spec; @@ -106,7 +106,7 @@ function setupBrowser() { backButton.disabled = !browser.canGoBack; forwardButton.disabled = !browser.canGoForward; }, - onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aWebProgress, aRequest, aStateFlags) { if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { // Network requests are complete. Disable (hide) the stop button // and enable (show) the refresh button @@ -153,7 +153,7 @@ function setupNavButtons() { "ePrefs", ]; - function navButtonHandler(e) { + function navButtonHandler() { if (!this.disabled) { switch (this.id) { case "eBack": diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 1b87a9ab4a..725a63981c 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -38,6 +38,7 @@ #ifdef MOZ_UPDATER @APPNAME@/Contents/Library/LaunchServices #endif +@APPNAME@/Contents/Frameworks @APPNAME@/Contents/PkgInfo @RESPATH@/firefox.icns @RESPATH@/document.icns @@ -141,8 +142,11 @@ #endif @RESPATH@/application.ini #ifdef MOZ_UPDATER +# update-settings.ini has been removed on macOS. +#ifndef XP_MACOSX @RESPATH@/update-settings.ini #endif +#endif @RESPATH@/platform.ini #ifndef MOZ_FOLD_LIBS @BINPATH@/@DLL_PREFIX@mozsqlite3@DLL_SUFFIX@ @@ -245,15 +249,15 @@ @RESPATH@/defaults/autoconfig/prefcalls.js @RESPATH@/browser/defaults/permissions ; Remote Settings JSON dumps -@RESPATH@/browser/defaults/settings/last_modified.json -@RESPATH@/browser/defaults/settings/blocklists -@RESPATH@/browser/defaults/settings/main -@RESPATH@/browser/defaults/settings/security-state +@RESPATH@/browser/defaults/settings +# channel-prefs.js has been removed on macOS. +#ifndef XP_MACOSX ; Warning: changing the path to channel-prefs.js can cause bugs (Bug 756325) ; Technically this is an app pref file, but we are keeping it in the original ; gre location for now. @RESPATH@/defaults/pref/channel-prefs.js +#endif ; Background tasks-specific preferences. ; These are in the GRE location since they apply to all tasks at this time. @@ -381,14 +385,10 @@ bin/libfreebl_64int_3.so @BINPATH@/crashreporter.app/ #else @BINPATH@/crashreporter@BIN_SUFFIX@ -@RESPATH@/crashreporter.ini -#ifdef XP_UNIX -@RESPATH@/Throbber-small.gif -#elif defined(XP_WIN) +#if defined(XP_WIN) @BINPATH@/@DLL_PREFIX@mozwer@DLL_SUFFIX@ #endif #endif -@RESPATH@/browser/crashreporter-override.ini #ifdef MOZ_CRASHREPORTER_INJECTOR @BINPATH@/breakpadinjector.dll #endif diff --git a/browser/installer/removed-files.in b/browser/installer/removed-files.in index 425978a5eb..ec9b6b075a 100644 --- a/browser/installer/removed-files.in +++ b/browser/installer/removed-files.in @@ -72,3 +72,15 @@ @DIR_RESOURCES@chrome.manifest #endif #endif + +# channel-prefs.js has been removed on macOS. +#ifdef XP_MACOSX +@DIR_RESOURCES@defaults/pref/channel-prefs.js +@DIR_RESOURCES@defaults/pref/ +@DIR_RESOURCES@defaults/ +#endif + +# update-settings.ini has been removed on macOS. +#ifdef XP_MACOSX +@DIR_RESOURCES@update-settings.ini +#endif diff --git a/browser/installer/windows/docs/FullInstaller.rst b/browser/installer/windows/docs/FullInstaller.rst index fcfa60371e..74e6ad047c 100644 --- a/browser/installer/windows/docs/FullInstaller.rst +++ b/browser/installer/windows/docs/FullInstaller.rst @@ -10,5 +10,7 @@ If it was not launched by the :doc:`StubInstaller`, an :ref:`Install Ping` is se The installer writes ``installation_telemetry.json`` to the install location, this is read by Firefox in order to send a telemetry event, see the event definition in `Events.yaml <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Events.yaml>`_ (category ``installation``, event name ``first_seen``) for a description of the properties. There is also an ``install_timestamp`` property, which is saved in the profile to determine whether there has been a new installation; this is not sent as part of the ping. +The full installer can also access NSIS plugins written in C++, see :doc:`building NSIS plugins <NSISPlugins>` for more information. + .. toctree:: FullConfig diff --git a/browser/installer/windows/docs/NSISPlugins.rst b/browser/installer/windows/docs/NSISPlugins.rst new file mode 100644 index 0000000000..77b766999f --- /dev/null +++ b/browser/installer/windows/docs/NSISPlugins.rst @@ -0,0 +1,101 @@ +===================== +Building NSIS Plugins +===================== + +.. note:: + + This guide assumes that you have a Firefox build environment set up as well as a recent version of Visual Studio. The steps here use Visual Studio 2022. + +Instructions +------------ + +1. Make sure you are configured to build DLLs. Follow this `guide <https://learn.microsoft.com/en-us/cpp/build/walkthrough-creating-and-using-a-dynamic-link-library-cpp>`_. +2. NSIS plugins are not integrated with the build system pending `bug 1771192 <https://bugzilla.mozilla.org/show_bug.cgi?id=1771192>`_. You will need to build them manually by creating a new Visual Studio project in the ``$SRCDIR/other-licenses/nsis/Contrib/`` directory with the following settings. + +.. image:: newProjectDllVS.png +.. image:: projectSettingsDllVS.png + +3. Once the project has been created, right click on it in the sidebar and go to ``Configuration Properties -> C/C++ -> Precompiled Header`` and set ``Precompiled Header`` to "Not Using Precompiled Headers". + +.. image:: projectPropertyPageVS.png + +4. For easier testing set the output directory in ``Configuration Properties -> General`` to ``$SRCDIR/other-licenses/nsis/Plugins``. + +5. Delete any files generated when you created the Visual Studio project such as ``pch.h`` or ``framework.h`` and any related include statements. + +6. Download the source code for `NSIS version 3.07 <https://sourceforge.net/projects/nsis/files/NSIS%203/3.07/>`_. (current at the time of writing although possibly subject to change) and extract the source files. Navigate to ``Contrib/ExDLL`` and copy ``pluginapi.h``, ``pluginapi.c`` and ``nsis_tchar.h`` to where header files for your Visual Studio project live. Add them to your project. + +7. You can use the following template to get started with your own plugin: + +.. code:: cpp + + /* 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/. */ + + // Put a brief description of your NSIS plugin here. + + // Put your include statements here. + #include <sysheader> + #include "pluginapi.h" // This is taken from the NSIS plugin page + #include "myheader.h" + + // A struct used for reading the stack passed in to the function + struct stack_t { + stack_t* next; + TCHAR text[MAX_PATH]; + }; + + /** + * + * + * Put any additional functions you write here. + * + * + */ + + // I use popstringn and pushstringn from the NSIS pluginapi.h file. + + // This is the function I want to call from within NSIS + extern "C" void __declspec(dllexport) + MyNSISFunction(HWND, int string_size, TCHAR* variables, stack_t** stacktop, void*) { + wchar_t getArg[MAX_PATH+1]; + EXDLL_INIT(); + bool rv = false; + int popRet = popstringn(getArg, MAX_PATH+1); + if (popRet == 0) { + rv = FunctionThatTakesAnArgument(getArg); + } + pushstring(rv ? L"1" : L"0"); + } + + BOOL APIENTRY + DllMain(HMODULE, DWORD, LPVOID) { + return TRUE; + } + +8. Modify ``$SRCDIR/toolkit/mozapps/installer/windows/nsis/makensis.mk`` as follows: + +.. code:: text + + CUSTOM_NSIS_PLUGINS = \ + ... \ + MyPlugin.dll \ + ... \ + $(NULL) + + +9. **NSIS only works with 32-bit plugins so ensure your Visual Studio build configuration is set to x86.** Compile your new plugin. ``exp`` and ``lib`` files will also be generated but they can safely be deleted. + +10. The plugin can now be called from within NSIS as follows: + +.. code:: text + + MyPlugin::MyNSISFunc "$myNSISarg" + +.. note:: + + - You may need to run ``./mach clobber`` for your DLL to be recognized. + - You can compile your plugin in debug mode and step through it with a debugger by attaching to the installer/uninstall process. + - If libraries are needed, files in the ``$SRCDIR/mfbt/`` and ``$SRCDIR/toolkit/`` directories are usually okay although there may be exceptions. + - The best way to access headers is usually to simply copy them into the project given how disconnected this is from the rest of the build system. diff --git a/browser/installer/windows/docs/newProjectDllVS.png b/browser/installer/windows/docs/newProjectDllVS.png Binary files differnew file mode 100644 index 0000000000..fe82d3e03a --- /dev/null +++ b/browser/installer/windows/docs/newProjectDllVS.png diff --git a/browser/installer/windows/docs/projectPropertyPageVS.png b/browser/installer/windows/docs/projectPropertyPageVS.png Binary files differnew file mode 100644 index 0000000000..3d1ecb6083 --- /dev/null +++ b/browser/installer/windows/docs/projectPropertyPageVS.png diff --git a/browser/installer/windows/docs/projectSettingsDllVS.png b/browser/installer/windows/docs/projectSettingsDllVS.png Binary files differnew file mode 100644 index 0000000000..59329df681 --- /dev/null +++ b/browser/installer/windows/docs/projectSettingsDllVS.png diff --git a/browser/locales-preview/select-translations.ftl b/browser/locales-preview/select-translations.ftl index 731abaff1a..39c882ec64 100644 --- a/browser/locales-preview/select-translations.ftl +++ b/browser/locales-preview/select-translations.ftl @@ -47,5 +47,8 @@ select-translations-panel-done-button = Done # Text displayed on translate-full-page button. select-translations-panel-translate-full-page-button = Translate full page -# Text displayed as a placeholder in the translated text area. -select-translations-panel-placeholder-text = Translated text will appear here. +# Text displayed as a placeholder when the panel is idle. +select-translations-panel-idle-placeholder-text = Translated text will appear here. + +# Text displayed as a placeholder when the panel is actively translating. +select-translations-panel-translating-placeholder-text = Translating… diff --git a/browser/locales-preview/translations.ftl b/browser/locales-preview/translations.ftl index 2da77126d1..fa0d0a7faf 100644 --- a/browser/locales-preview/translations.ftl +++ b/browser/locales-preview/translations.ftl @@ -19,5 +19,6 @@ translations-settings-never-sites-description = To add to this list, visit a sit ## Section to download language models to enable offline translation. translations-settings-download-languages = Download languages +translations-settings-download-all-languages = All languages translations-settings-download-languages-link = Learn more about downloading languages -translations-settings-download-language = Language +translations-settings-language-header = Language diff --git a/browser/locales/en-US/browser/appmenu.ftl b/browser/locales/en-US/browser/appmenu.ftl index 5ad2d75f62..f9588a5053 100644 --- a/browser/locales/en-US/browser/appmenu.ftl +++ b/browser/locales/en-US/browser/appmenu.ftl @@ -81,7 +81,7 @@ appmenu-remote-tabs-turn-on-sync = # This is shown after the tabs list if we can display more tabs by clicking on the button appmenu-remote-tabs-showmore = - .label = Show More Tabs + .label = Show more tabs .tooltiptext = Show more tabs from this device # This is shown when there are inactive tabs which are not being shown. diff --git a/browser/locales/en-US/browser/defaultBrowserNotification.ftl b/browser/locales/en-US/browser/defaultBrowserNotification.ftl index 67be2c55a9..90b6a07e08 100644 --- a/browser/locales/en-US/browser/defaultBrowserNotification.ftl +++ b/browser/locales/en-US/browser/defaultBrowserNotification.ftl @@ -21,3 +21,20 @@ default-browser-prompt-message-alt = Get speed, safety, and privacy every time y default-browser-prompt-button-primary-alt = Set as default browser default-browser-prompt-checkbox-not-again-label = Don’t show this message again default-browser-prompt-button-secondary = Not now + +## Strings for a Windows native guidance notification when the user is forced to +## use Windows Settings to set the default browser. Instructions differ for +## Windows 10 and 11. + +default-browser-guidance-notification-title = Finish making { -brand-short-name } your default +# Quoted text are keywords to look for in the Windows Settings app. +default-browser-guidance-notification-body-instruction-win10 = + Step 1: Go to Settings > Default apps + Step 2: Scroll down to “Web browser” + Step 3: Select and choose { -brand-short-name } +# Quoted text are keywords to look for in the Windows Settings app. +default-browser-guidance-notification-body-instruction-win11 = + Step 1: Go to Settings > Default apps + Step 2: Select “Set default” for { -brand-short-name } +default-browser-guidance-notification-info-page = Show me +default-browser-guidance-notification-dismiss = Done diff --git a/browser/locales/en-US/browser/menubar.ftl b/browser/locales/en-US/browser/menubar.ftl index 802996627b..f65a345d60 100644 --- a/browser/locales/en-US/browser/menubar.ftl +++ b/browser/locales/en-US/browser/menubar.ftl @@ -139,6 +139,8 @@ menu-view-history-button = .label = History menu-view-synced-tabs-sidebar = .label = Synced Tabs +menu-view-megalist-sidebar = + .label = Passwords menu-view-full-zoom = .label = Zoom .accesskey = Z diff --git a/browser/locales/en-US/browser/newtab/newtab.ftl b/browser/locales/en-US/browser/newtab/newtab.ftl index 256a8da576..50bd34f717 100644 --- a/browser/locales/en-US/browser/newtab/newtab.ftl +++ b/browser/locales/en-US/browser/newtab/newtab.ftl @@ -272,3 +272,25 @@ newtab-custom-recent-toggle = .description = A selection of recent sites and content newtab-custom-close-button = Close newtab-custom-settings = Manage more settings + +## New Tab Wallpapers + +newtab-wallpaper-title = Wallpapers +newtab-wallpaper-reset = Reset to default +newtab-wallpaper-light-red-panda = Red panda +newtab-wallpaper-light-mountain = White mountain +newtab-wallpaper-light-sky = Sky with purple and pink clouds +newtab-wallpaper-light-color = Blue, pink and yellow shapes +newtab-wallpaper-light-landscape = Blue mist mountain landscape +newtab-wallpaper-light-beach = Beach with palm tree +newtab-wallpaper-dark-aurora = Aurora Borealis +newtab-wallpaper-dark-color = Red and blue shapes +newtab-wallpaper-dark-panda = Red panda hidden in forest +newtab-wallpaper-dark-sky = City landscape with a night sky +newtab-wallpaper-dark-mountain = Landscape mountain +newtab-wallpaper-dark-city = Purple city landscape + +# Variables +# $author_string (String) - The name of the creator of the photo. +# $webpage_string (String) - The name of the webpage where the photo is located. +newtab-wallpaper-attribution = Photo by <a data-l10n-name="name-link">{ $author_string }</a> on <a data-l10n-name="webpage-link">{ $webpage_string }</a> diff --git a/browser/locales/en-US/browser/newtab/onboarding.ftl b/browser/locales/en-US/browser/newtab/onboarding.ftl index 00c5da9305..0c66671f8a 100644 --- a/browser/locales/en-US/browser/newtab/onboarding.ftl +++ b/browser/locales/en-US/browser/newtab/onboarding.ftl @@ -55,6 +55,10 @@ mr1-onboarding-theme-header = Make it your own mr1-onboarding-theme-subtitle = Personalize { -brand-short-name } with a theme. mr1-onboarding-theme-secondary-button-label = Not now +newtab-wallpaper-onboarding-title = Try a splash of color +newtab-wallpaper-onboarding-subtitle = Choose a wallpaper to give your New Tab a fresh look. +newtab-wallpaper-onboarding-primary-button-label = Set wallpaper + # System theme uses operating system color settings mr1-onboarding-theme-label-system = System theme diff --git a/browser/locales/en-US/browser/policies/policies-descriptions.ftl b/browser/locales/en-US/browser/policies/policies-descriptions.ftl index 22c881c261..fcecab699a 100644 --- a/browser/locales/en-US/browser/policies/policies-descriptions.ftl +++ b/browser/locales/en-US/browser/policies/policies-descriptions.ftl @@ -221,6 +221,8 @@ policy-StartDownloadsInTempDirectory = Force downloads to start off in a local, policy-SupportMenu = Add a custom support menu item to the help menu. +policy-TranslateEnabled = Enable or disable webpage translation. + policy-UserMessaging = Don’t show certain messages to the user. policy-UseSystemPrintDialog = Print using the system print dialog. diff --git a/browser/locales/en-US/browser/preferences/preferences.ftl b/browser/locales/en-US/browser/preferences/preferences.ftl index a3c382cac3..c25b299ae1 100644 --- a/browser/locales/en-US/browser/preferences/preferences.ftl +++ b/browser/locales/en-US/browser/preferences/preferences.ftl @@ -38,6 +38,8 @@ search-input-box2 = .placeholder = Find in Settings managed-notice = Your browser is being managed by your organization. +managed-notice-info-icon = + .alt = Information category-list = .aria-label = Categories @@ -853,8 +855,15 @@ sync-mobile-promo = Download Firefox for <img data-l10n-name="android-icon"/> <a ## Firefox account - Signed in -sync-profile-picture = +sync-profile-picture-with-alt = .tooltiptext = Change profile picture + .alt = Change profile picture + +sync-profile-picture-account-problem = + .alt = Account profile picture + +fxa-login-rejected-warning = + .alt = Warning sync-sign-out = .label = Sign Out… @@ -1586,8 +1595,6 @@ preferences-doh-checkbox-warn = preferences-doh-select-resolver = Choose provider: -preferences-doh-exceptions-description = { -brand-short-name } won’t use secure DNS on these sites - preferences-doh-manage-exceptions = .label = Manage Exceptions… .accesskey = x diff --git a/browser/locales/en-US/browser/reportBrokenSite.ftl b/browser/locales/en-US/browser/reportBrokenSite.ftl index 428b6b15ee..3c44110bc1 100644 --- a/browser/locales/en-US/browser/reportBrokenSite.ftl +++ b/browser/locales/en-US/browser/reportBrokenSite.ftl @@ -21,7 +21,7 @@ report-broken-site-panel-reason-media = report-broken-site-panel-reason-content = .label = Buttons, links, and other content report-broken-site-panel-reason-account = - .label = Sign-in or Sign-out + .label = Sign-in or sign-out report-broken-site-panel-reason-adblockers = .label = Ad blockers report-broken-site-panel-reason-other = diff --git a/browser/locales/en-US/browser/screenshots.ftl b/browser/locales/en-US/browser/screenshots.ftl index 1ccf631ae1..ea2e42191c 100644 --- a/browser/locales/en-US/browser/screenshots.ftl +++ b/browser/locales/en-US/browser/screenshots.ftl @@ -23,8 +23,6 @@ screenshots-copy-button-title = .title = Copy screenshot to clipboard screenshots-cancel-button-title = .title = Cancel -screenshots-retry-button-title = - .title = Retry screenshot screenshots-meta-key = { PLATFORM() -> @@ -58,3 +56,46 @@ screenshots-generic-error-details = We’re not sure what just happened. Care to screenshots-too-large-error-title = Your screenshot was cropped because it was too large screenshots-too-large-error-details = Try selecting a region that’s smaller than 32,700 pixels on its longest side or 124,900,000 pixels total area. + +screenshots-component-retry-button = + .title = Retry screenshot + .aria-label = Retry screenshot + +screenshots-component-cancel-button = + .title = + { PLATFORM() -> + [macos] Cancel (esc) + *[other] Cancel (Esc) + } + .aria-label = Cancel + +# Variables +# $shortcut (String) - A keyboard shortcut for copying the screenshot. +screenshots-component-copy-button = + .title = Copy ({ $shortcut }) + .aria-label = Copy + +screenshots-component-copy-button-label = Copy + +# Variables +# $shortcut (String) - A keyboard shortcut for saving/downloading the screenshot. +screenshots-component-download-button = + .title = Download ({ $shortcut }) + .aria-label = Download + +screenshots-component-download-button-label = Download + +## The below strings are used to capture keydown events so the strings should +## not be changed unless the keyboard layout in the locale requires it. + +screenshots-component-download-key = S +screenshots-component-copy-key = C + +## + +# This string represents the selection size area +# "x" here represents "by" (i.e 123 by 456) +# Variables: +# $width (Number) - The width of the selection region in pixels +# $height (Number) - The height of the selection region in pixels +screenshots-overlay-selection-region-size-2 = { $width } x { $height } diff --git a/browser/locales/en-US/browser/screenshotsOverlay.ftl b/browser/locales/en-US/browser/screenshotsOverlay.ftl deleted file mode 100644 index ab8d8740a1..0000000000 --- a/browser/locales/en-US/browser/screenshotsOverlay.ftl +++ /dev/null @@ -1,15 +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/. - -screenshots-overlay-cancel-button = Cancel -screenshots-overlay-instructions = Drag or click on the page to select a region. Press ESC to cancel. -screenshots-overlay-download-button = Download -screenshots-overlay-copy-button = Copy - -# This string represents the selection size area -# "x" here represents "by" (i.e 123 by 456) -# Variables: -# $width (Number) - The width of the selection region in pixels -# $height (Number) - The height of the selection region in pixels -screenshots-overlay-selection-region-size = { $width } x { $height } diff --git a/browser/locales/en-US/browser/sidebarMenu.ftl b/browser/locales/en-US/browser/sidebarMenu.ftl index 746a2084df..e050a2302c 100644 --- a/browser/locales/en-US/browser/sidebarMenu.ftl +++ b/browser/locales/en-US/browser/sidebarMenu.ftl @@ -11,6 +11,9 @@ sidebar-menu-history = sidebar-menu-synced-tabs = .label = Synced Tabs +sidebar-menu-megalist = + .label = Passwords + sidebar-menu-close = .label = Close Sidebar diff --git a/browser/locales/en-US/browser/webProtocolHandler.ftl b/browser/locales/en-US/browser/webProtocolHandler.ftl index 848e5d469b..e11ec0deac 100644 --- a/browser/locales/en-US/browser/webProtocolHandler.ftl +++ b/browser/locales/en-US/browser/webProtocolHandler.ftl @@ -2,20 +2,13 @@ # 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/. -protocolhandler-mailto-os-handler-notificationbox = Always use { -brand-short-name } to open links that send email? -protocolhandler-mailto-os-handler-yes-confirm = { -brand-short-name } is now your default application for opening links that send email. protocolhandler-mailto-os-handler-yes-button = Set as default protocolhandler-mailto-os-handler-no-button = Not now ## Variables: ## $url (String): The url of a webmailer, but only its full domain name. -protocolhandler-mailto-handler-notificationbox-always = Always open email links using { $url }? -protocolhandler-mailto-handler-yes-confirm = { $url } is now your default site for opening links that send email. -protocolhandler-mailto-handler-set-message = Use <strong>{ $url } in { -brand-short-name }</strong> every time you click a link that opens your email? -protocolhandler-mailto-handler-confirm-message = <strong>{ $url } in { -brand-short-name }</strong> is now your computer’s default email handler. +protocolhandler-mailto-handler-set = Use <strong>{ -brand-short-name } to open { $url }</strong> every time you click a link that opens your email? +protocolhandler-mailto-handler-confirm = <strong>{ -brand-short-name } will open { $url }</strong> every time you click a link that sends email. ## - -protocolhandler-mailto-handler-yes-button = Set as default -protocolhandler-mailto-handler-no-button = Not now diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index c326f2843c..a1d25d093a 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -150,9 +150,10 @@ webauthn.uvBlockedPrompt=User verification failed on %S. There were too many fai webauthn.alreadyRegisteredPrompt=This device is already registered. Try a different device. webauthn.cancel=Cancel webauthn.cancel.accesskey=c -webauthn.proceed=Proceed -webauthn.proceed.accesskey=p -webauthn.anonymize=Anonymize anyway +webauthn.allow=Allow +webauthn.allow.accesskey=A +webauthn.block=Block +webauthn.block.accesskey=B # LOCALIZATION NOTE (identity.identified.verifier, identity.identified.state_and_country, identity.ev.contentOwner2): # %S is the hostname of the site that is being displayed. diff --git a/browser/locales/en-US/crashreporter/crashreporter-override.ini b/browser/locales/en-US/crashreporter/crashreporter-override.ini deleted file mode 100644 index f14b1c4f0d..0000000000 --- a/browser/locales/en-US/crashreporter/crashreporter-override.ini +++ /dev/null @@ -1,9 +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/. - -# This file is in the UTF-8 encoding -[Strings] -# LOCALIZATION NOTE (CrashReporterProductErrorText2): The %s is replaced with a string containing detailed information. -CrashReporterProductErrorText2=Firefox had a problem and crashed. We’ll try to restore your tabs and windows when it restarts.\n\nUnfortunately the crash reporter is unable to submit a crash report.\n\nDetails: %s -CrashReporterDescriptionText2=Firefox had a problem and crashed. We’ll try to restore your tabs and windows when it restarts.\n\nTo help us diagnose and fix the problem, you can send us a crash report. diff --git a/browser/locales/jar.mn b/browser/locales/jar.mn index d42a40075e..51cd7853b0 100644 --- a/browser/locales/jar.mn +++ b/browser/locales/jar.mn @@ -13,6 +13,7 @@ preview/interventions.ftl (../components/urlbar/content/interventions.ftl) preview/enUS-searchFeatures.ftl (../components/urlbar/content/enUS-searchFeatures.ftl) preview/shopping.ftl (../components/shopping/content/shopping.ftl) + preview/sidebar.ftl (../components/sidebar/sidebar.ftl) preview/onboarding.ftl (../components/aboutwelcome/content/onboarding.ftl) preview/select-translations.ftl (../locales-preview/select-translations.ftl) preview/translations.ftl (../locales-preview/translations.ftl) diff --git a/browser/locales/l10n-changesets.json b/browser/locales/l10n-changesets.json index 63c677409f..4a9662810e 100644 --- a/browser/locales/l10n-changesets.json +++ b/browser/locales/l10n-changesets.json @@ -5,6 +5,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -15,7 +16,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "e56906155af0a796ae189e674cab3f8bf743a1d7" + "revision": "01d0b95d9e814dbe3498ffcc4826536e7bfa982d" }, "af": { "pin": false, @@ -23,6 +24,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -33,7 +35,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "8c753ad94ecbf725f4466abf3a376cd38e4a1831" + "revision": "4b4e1ac99efc91c8d452e2fa6493443cd41f7bdd" }, "an": { "pin": false, @@ -41,6 +43,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -51,7 +54,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "2260f7621d0c931e75cd87979083f3b562c87977" + "revision": "a7fb8ee7157c3256bb2bc41ed7de2e32c8fc7fb2" }, "ar": { "pin": false, @@ -59,6 +62,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -69,7 +73,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "b4d37327b39820f3169bb9b9b613f96ea48d00ca" + "revision": "c25d000804793caac2244435604876fa7c7ac53b" }, "ast": { "pin": false, @@ -77,6 +81,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -87,7 +92,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "8add1fc16c021b09780dd7ead4006ca211371104" + "revision": "9e8adc849e5fd31664f6b34f9c94a9e68d8a17b4" }, "az": { "pin": false, @@ -95,6 +100,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -105,7 +111,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "74562508d84ac13b4ae018f9183d92cc446daf9a" + "revision": "f9a497246603da23af3f8f7cfb6eb93c329fcfa0" }, "be": { "pin": false, @@ -113,6 +119,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -123,7 +130,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "5a6aabcf82699167d67efde3811966590ba35b30" + "revision": "2baa906a4a1c5d6d035cf74c3a0a26ed0b9abb10" }, "bg": { "pin": false, @@ -131,6 +138,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -141,7 +149,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "77a7f14bc07ba99f73c7ec222549b782a103946b" + "revision": "9f8e00927a58304657f69e68c45bb08b283c7bb4" }, "bn": { "pin": false, @@ -149,6 +157,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -159,7 +168,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "82f5498c867bc61944df5c1ee8b97e2589f2f0a3" + "revision": "d8cff26d13b63763623a45fc1e0ac2346b1b07ad" }, "br": { "pin": false, @@ -167,6 +176,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -177,7 +187,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "c0616c261b890d821ba66f75007c1518f8d1c910" + "revision": "90580336afef095faa56a66bf4d002c39bd78733" }, "bs": { "pin": false, @@ -185,6 +195,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -195,7 +206,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "9b859c2c4e52adbaed8116c763c20748c47570ec" + "revision": "80e6a955d8735d71a89cfb6ac8a76dbee6b47538" }, "ca": { "pin": false, @@ -203,6 +214,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -213,7 +225,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "cdeba30f234c8e7f39fddb766483e0408825fd0b" + "revision": "2de60e3d6d0cadadfe9115752aaad0bd7223c98a" }, "ca-valencia": { "pin": false, @@ -221,6 +233,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -231,7 +244,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "b506882a00f95d37c2fba501fbf4559b7ea46bc5" + "revision": "e79bb891751b8859609abe02840780b675002131" }, "cak": { "pin": false, @@ -239,6 +252,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -249,7 +263,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "a8bf4d8c2d18e661b16be19234ef33da6e582e8e" + "revision": "87a4bdc479c2957a4a336691e8c30566f718392d" }, "cs": { "pin": false, @@ -257,6 +271,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -267,7 +282,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "4e45324c27df1a2b284faa92f569736e7c14177d" + "revision": "18be7c518fc6e8d74f49a496a0e6d5e4bfeef7d3" }, "cy": { "pin": false, @@ -275,6 +290,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -285,7 +301,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "3ac8ac45172a9742d37787b29202c10b27c81f02" + "revision": "9a284d9a640870b3a94bc40ec6c410539ba13e6c" }, "da": { "pin": false, @@ -293,6 +309,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -303,7 +320,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "5ebe2c380a51751f9f7e68f87d58b217502f1cbe" + "revision": "19a69f89deb36dae9db108292461e6be329302d9" }, "de": { "pin": false, @@ -311,6 +328,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -321,7 +339,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "3554b47092ee0879c578a4d22053c2ea33570d03" + "revision": "467cfd37c3bdf242b76a72d901ac0a80306d3e0b" }, "dsb": { "pin": false, @@ -329,6 +347,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -339,7 +358,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "82d5ae3cf66ee96f461a27d8073802ac1a61c792" + "revision": "299b082e1a91ef8acb6446de73f51aee1f2a2622" }, "el": { "pin": false, @@ -347,6 +366,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -357,7 +377,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "22ab7ff93b039239218bdc9592a7944d572b4b9e" + "revision": "b25d9d08f10f517cec0ade35539f02e58d31b3d5" }, "en-CA": { "pin": false, @@ -365,6 +385,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -375,7 +396,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "2a1827551017958cb228abe4a3eb3e9aae8fb821" + "revision": "15e1642619360a1b4492014fa32fa6644ad64ae4" }, "en-GB": { "pin": false, @@ -383,6 +404,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -393,7 +415,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "4af2f6747560464de71814cbd52d98b97c608094" + "revision": "9de9183c97d3bfaa0a90fb7e8b466070c35e25f3" }, "eo": { "pin": false, @@ -401,6 +423,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -411,7 +434,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "2d703c9b37c361b9a53e5f01d831c62a373d8367" + "revision": "063873f0ce4a11dd574309524a069439050988e5" }, "es-AR": { "pin": false, @@ -419,6 +442,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -429,7 +453,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "c4eecaa5bf036b06959d1a240ed42292133a2129" + "revision": "42228ca4434593daaf0c00730bdb49fc82380082" }, "es-CL": { "pin": false, @@ -437,6 +461,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -447,7 +472,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "c73a07524dd2922d14b27ee86c54d70374ca21bf" + "revision": "78d913d7193ab71fa909f7f029874be302672c09" }, "es-ES": { "pin": false, @@ -455,6 +480,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -465,7 +491,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "ba37303d99ac5b7bfa19d19085de59d932300bbc" + "revision": "beff1baac7c5d71e84eb26c356472af091a82d48" }, "es-MX": { "pin": false, @@ -473,6 +499,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -483,7 +510,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "3b9d7105b936e46f9532f04886331ff6f5186bd6" + "revision": "175f671c5b64e680d2fb80bd676d6e4469f8f2e4" }, "et": { "pin": false, @@ -491,6 +518,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -501,7 +529,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "82cc93f22641af566f66068ca7d5c177658e79ab" + "revision": "79be34d00ff06d857ad720b8aedf553b6c402501" }, "eu": { "pin": false, @@ -509,6 +537,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -519,7 +548,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "e1f3ce7ff79d93dbbc8a7aaac805474addd8358a" + "revision": "f0f0a5d2807fdfe77f7d3eb4ed6eee0cc3997765" }, "fa": { "pin": false, @@ -527,6 +556,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -537,7 +567,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "8aa37d16a02797c0b6ed1ddf71faab9984f43c21" + "revision": "ebe0b60b0b367779c55a807661e383696401f6b2" }, "ff": { "pin": false, @@ -545,6 +575,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -555,7 +586,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "4c1c01424d31f9750019ff0e7aff05163dccdbda" + "revision": "a437fb813f99bb8acb221a8db1000cd078e0bfee" }, "fi": { "pin": false, @@ -563,6 +594,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -573,7 +605,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "a5d3c82f488c83536e5b17cebfb19db189efeae4" + "revision": "c54c3f938b4c5f4d956c4835929cb42676d916df" }, "fr": { "pin": false, @@ -581,6 +613,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -591,7 +624,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "a67337b0c59cb9c0703a3c932cf1bc47a66c4ad8" + "revision": "496c2eb73b82d5bb7fc3a10ccd159f3df0d76e7c" }, "fur": { "pin": false, @@ -599,6 +632,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -609,7 +643,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "aa25bbac8578a1c3410f9d65ce6853b35e3cec1f" + "revision": "e2d66ddab3ec57c1c4bd480e57786c4970a5bd12" }, "fy-NL": { "pin": false, @@ -617,6 +651,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -627,7 +662,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "40edbbc262b8395ea662973e3fff8f39a4746852" + "revision": "b9ae5d7ce6f436f239478b2a6386edaa9cc6757a" }, "ga-IE": { "pin": false, @@ -635,6 +670,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -645,7 +681,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "76f9eea7b23ec87fcdd076869ce924bab142c1cd" + "revision": "2fcccb5b19b300f4368399f83f38c962de907e6f" }, "gd": { "pin": false, @@ -653,6 +689,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -663,7 +700,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "2a6331654a8e611512f4373e1e49b97bc8d8cdbb" + "revision": "f89f7d0090410bd6071fdd10acdb725fd42dc683" }, "gl": { "pin": false, @@ -671,6 +708,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -681,7 +719,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "cf6c33c3245e8270981eff4eaa546114735381c8" + "revision": "d60053a8338abc292231feb6c72a480cbfe09282" }, "gn": { "pin": false, @@ -689,6 +727,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -699,7 +738,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "47c4da96bf4b6d275fc72bfd03d95b69cad110d1" + "revision": "abfa33a3851294650616f0916296b11fac8a7b26" }, "gu-IN": { "pin": false, @@ -707,6 +746,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -717,7 +757,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "e48fd7ae26766c8abdf195e4f11e84ab27e67bcd" + "revision": "6e7a555c8a2d8a0df4791c49636b735b2e3e892b" }, "he": { "pin": false, @@ -725,6 +765,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -735,7 +776,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "d9c3db88424d8beea2b60bd78999284b08bfb3d4" + "revision": "998b4095ec11c87d25de97aef4a8bf28016f05b8" }, "hi-IN": { "pin": false, @@ -743,6 +784,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -753,7 +795,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "bfbb8f977ba1ff43b982199cb333882f31f1e45c" + "revision": "3226adb8b31f20b5a1e511c39d96475bd981c64f" }, "hr": { "pin": false, @@ -761,6 +803,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -771,7 +814,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "e7cd43f2d9960b7f63de055ef1f7209aa7a3a59d" + "revision": "da0cae20d707fde4238bcb079bc9c9a572ca70ad" }, "hsb": { "pin": false, @@ -779,6 +822,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -789,7 +833,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "49e9db6cad9b7bfc4f1674efa68ddb3f7ebe4a31" + "revision": "1e13c9b155321cb9b904386d5c90736baaf52f9b" }, "hu": { "pin": false, @@ -797,6 +841,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -807,7 +852,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "3c019f8a611ddb463f5b2c027db6e1aeb7bb4cba" + "revision": "5a76dd3b5d5ca290e2d3bed08cd2378c0a89f4bc" }, "hy-AM": { "pin": false, @@ -815,6 +860,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -825,7 +871,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "8a1b4667e6cec77ed8c0a18617f7eb7e05b4c868" + "revision": "2667935d01d797f7f8b25a461c1ab4d5388b827e" }, "ia": { "pin": false, @@ -833,6 +879,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -843,7 +890,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "bb783a3c8b55dd6b59617d297a12a2bd67c5e168" + "revision": "0a8e921a71ddea28aa8279d9cc30a2c228514285" }, "id": { "pin": false, @@ -851,6 +898,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -861,7 +909,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "90a2f0dc84d75f59927860b2049868eb333316e7" + "revision": "6e6de17dcac401dbb916db954c39c571c13995bf" }, "is": { "pin": false, @@ -869,6 +917,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -879,7 +928,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "9ea9da23afcf96a31313ec96c36211ebb7eed93c" + "revision": "536265635dfec9c1f466d9716b9ef0ad28ac7a09" }, "it": { "pin": false, @@ -887,6 +936,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -897,7 +947,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "ebd49136fb8e97c46b214c384a0eb4a505a7f6e2" + "revision": "cc7ac99088956c23581aaf6f166a7ae1645e88b4" }, "ja": { "pin": false, @@ -905,6 +955,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "win32", "win32-devedition", @@ -913,7 +964,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "e7a4e3dfae9f263193befecdd5e95efed3a30259" + "revision": "dc1072879ff7584807d0924a345d813293504dcb" }, "ja-JP-mac": { "pin": false, @@ -921,7 +972,7 @@ "macosx64", "macosx64-devedition" ], - "revision": "6a7a8218314e397ea975f8229d25946d71b59f93" + "revision": "ee795c6312422f18ad974bdba9cd85048602a8c4" }, "ka": { "pin": false, @@ -929,6 +980,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -939,7 +991,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "ae487eeedbef5a136b9fa248bdc7a00a33d70cb1" + "revision": "d0819a64fc401ea706eb618da80438765b636aeb" }, "kab": { "pin": false, @@ -947,6 +999,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -957,7 +1010,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "4d08d247633ee72779ff30b86a0bdec45ccdedaf" + "revision": "f2e6e32f3903bd964eca9919dad4ce24c69aa61f" }, "kk": { "pin": false, @@ -965,6 +1018,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -975,7 +1029,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "ed4a829e6aa5280f06d614828f8fdaec7b1da2eb" + "revision": "0d31157370f1ccafab11b1a144de43d227512e2e" }, "km": { "pin": false, @@ -983,6 +1037,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -993,7 +1048,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "b0c9be220e4029173257e1ed0f790fa4b4040206" + "revision": "34fd1eee4268442bde103bae424da5e03350ef01" }, "kn": { "pin": false, @@ -1001,6 +1056,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1011,7 +1067,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "5aacf1c710ae9c0c2b1c1796010ea4a129c03f36" + "revision": "5290237e824838c10238776252c2404eafd17318" }, "ko": { "pin": false, @@ -1019,6 +1075,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1029,7 +1086,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "bca7fe06347fee6da026f9ab689c7f3671dd46a1" + "revision": "6ef881aff44bb92e5a6a95f369064c4c339620b9" }, "lij": { "pin": false, @@ -1037,6 +1094,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1047,7 +1105,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "5d53f2add9e137e357ef51d5ea43f6b798c41e80" + "revision": "d5074f9a22e6fa13c5ee8abc60636ce37e658716" }, "lt": { "pin": false, @@ -1055,6 +1113,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1065,7 +1124,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "45842f645c5566ed254f2c8ec73cffd5c7af7ea1" + "revision": "afcbc29a15e5429879fd336d16e2627b57549e31" }, "lv": { "pin": false, @@ -1073,6 +1132,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1083,7 +1143,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "0f64a1f51d1db65ed999c63c39124af2f547c96c" + "revision": "9e9a2a3c2432a27f48559d94f26e2a207dc99d84" }, "mk": { "pin": false, @@ -1091,6 +1151,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1101,7 +1162,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "ecc37fdf059b2a1a6230f02f3bc2214ca7015d27" + "revision": "84f3d6c7e2dacce64f1ec9eb845a2ba1f6ca5849" }, "mr": { "pin": false, @@ -1109,6 +1170,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1119,7 +1181,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "4a5b2fbc3c5f256eee073b445008b0b005488f5b" + "revision": "e5561a32b37ee3e26032b1ab534df138de588e49" }, "ms": { "pin": false, @@ -1127,6 +1189,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1137,7 +1200,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "e90e08a2ea1300bf975f42efed7f2194114e1dbd" + "revision": "c9ec27a5db3da7cec0ae60128506692f6dcbe17e" }, "my": { "pin": false, @@ -1145,6 +1208,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1155,7 +1219,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "23501cb36dc4035c7a27390609eb24e035e1a005" + "revision": "5c1480ccc04021dbf4fde8d04ffab591d26fe80b" }, "nb-NO": { "pin": false, @@ -1163,6 +1227,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1173,7 +1238,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "fc7cd59756f2c5569b5d08285b0dbcfbd448afd9" + "revision": "fc1896a0a24d1ae5708a76aea1f437fda566ce84" }, "ne-NP": { "pin": false, @@ -1181,6 +1246,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1191,7 +1257,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "5b927b8baa831973766bd3cb67467fa9904ba4ba" + "revision": "8c9eac262db978fca8fc3a5c70f032bc3f3776ff" }, "nl": { "pin": false, @@ -1199,6 +1265,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1209,7 +1276,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "aa3f538ecf37090fc954c761e796ea6218f319ef" + "revision": "7e6da4f01bdbfd9615d541073109a1dd1371792e" }, "nn-NO": { "pin": false, @@ -1217,6 +1284,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1227,7 +1295,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "1a0dd05416e684f4dde8e04a51e035ccc6110913" + "revision": "c3d8b68621dab95b03dbb55b1fa61b7662eb6444" }, "oc": { "pin": false, @@ -1235,6 +1303,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1245,7 +1314,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "2803a34b9c0b40748a486ed6466ff1008f47b386" + "revision": "a550cf8ec350953cf895a3f84f7e657965b17303" }, "pa-IN": { "pin": false, @@ -1253,6 +1322,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1263,7 +1333,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "269fae1ddbcf6883566e70921ad6880a71638d9a" + "revision": "7531f541ae0ac61c3579417475e67ae5d399edfa" }, "pl": { "pin": false, @@ -1271,6 +1341,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1281,7 +1352,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "6b35b3b8a145ba2369afdab589397a76441523e8" + "revision": "821ebcda1289288364f72a18b74fa7651411aeaa" }, "pt-BR": { "pin": false, @@ -1289,6 +1360,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1299,7 +1371,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "a8b332979057da4a2e94e4a74aafb80dd5f3fd4d" + "revision": "94c3dbb67a5d8c6047f4a16750de0297e5e55b3b" }, "pt-PT": { "pin": false, @@ -1307,6 +1379,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1317,7 +1390,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "a8d851e526c7a54a89a11ecc3cf7791d45446410" + "revision": "76783945baff030a643ea073164d64ffd35937b0" }, "rm": { "pin": false, @@ -1325,6 +1398,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1335,7 +1409,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "21ea09d77e7d029548dacd52892834e297b04be4" + "revision": "12b4120f8968cd6a34805f44e9ea1edbc7e60699" }, "ro": { "pin": false, @@ -1343,6 +1417,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1353,7 +1428,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "4da561447a2729bc9aa95738360865e61170b787" + "revision": "470b13b5805b83864f3d6acee12747dfe38703de" }, "ru": { "pin": false, @@ -1361,6 +1436,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1371,7 +1447,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "fff022cef28026adec986585740239c632280cf2" + "revision": "402b2ecbf04d3e3ab9258f103d4e97e7ea2d107c" }, "sat": { "pin": false, @@ -1379,6 +1455,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1389,7 +1466,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "71c6e0f2fe93a7c505119333e4349d130a5ebd01" + "revision": "dec2e53c6e22f8bde70719d3f866e9000d61b0bf" }, "sc": { "pin": false, @@ -1397,6 +1474,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1407,7 +1485,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "339ce493f1f4a4acdae85fa0e941691df07457ab" + "revision": "a2964e3447a3a8de26ceb31fe8f31273bb4fa050" }, "sco": { "pin": false, @@ -1415,6 +1493,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1425,7 +1504,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "3cd439ea57adda8bd01176c959f4de7b3f254ab5" + "revision": "b0e67df5c86da77177388bcd8010bd22445e2230" }, "si": { "pin": false, @@ -1433,6 +1512,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1443,7 +1523,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "16d95740c1146559d807271882dfa808c510d8e6" + "revision": "05344a3721f083f18ba8fa427f742939724eb409" }, "sk": { "pin": false, @@ -1451,6 +1531,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1461,7 +1542,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "cfa944ce2cca7626804ecfabf3d980641ad638de" + "revision": "af01c2969bb64ea5493ff7997ba318ff928e5065" }, "sl": { "pin": false, @@ -1469,6 +1550,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1479,7 +1561,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "1bbc747d45749f3de133af03e2cf6d6e2794c7eb" + "revision": "0922a722c220a5403e08ea8460fe92180fd3dc96" }, "son": { "pin": false, @@ -1487,6 +1569,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1497,7 +1580,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "e2db0bfbf2e38ccac8c123e2b16b95e404dbc4eb" + "revision": "fc8d6dee9c1a803a0f6ef9f51583a47068301269" }, "sq": { "pin": false, @@ -1505,6 +1588,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1515,7 +1599,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "4a811dbbdb3ea0e92bfeac95e33f8106ee82187b" + "revision": "f637484e72b685f254224dbb7dc65f45414f6f91" }, "sr": { "pin": false, @@ -1523,6 +1607,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1533,7 +1618,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "c2645ef2665041649bbdacb43dc01242a89ec85d" + "revision": "944c72f995e4c8983920eb88cacbf187e145407d" }, "sv-SE": { "pin": false, @@ -1541,6 +1626,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1551,7 +1637,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "cfa68462018f9f0d4b2454ea0503ba75cc647a02" + "revision": "bb2d5d96d69ef9fb98ed291232344aa863e237bd" }, "szl": { "pin": false, @@ -1559,6 +1645,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1569,7 +1656,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "8fbd57ec396095a2fd07d810b0dd9f51f30d932c" + "revision": "a8afff859aca86ae43810fcf8b6b45a5ed753ad4" }, "ta": { "pin": false, @@ -1577,6 +1664,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1587,7 +1675,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "eac10e8865040bd62e2cfacaec0c7c1698e88c26" + "revision": "19b03e8569563cb3e5991a27e980baf897b88bd1" }, "te": { "pin": false, @@ -1595,6 +1683,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1605,7 +1694,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "d5e4f05dfa7a2f5c86a9b2f31c5e7f548e235950" + "revision": "4e32fe69464240002479e3c87ef13f039166c9ce" }, "tg": { "pin": false, @@ -1613,6 +1702,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1623,7 +1713,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "43e52dbaf04a78db93830490e60a419b1b9f94bd" + "revision": "ca844186ed55b5de8c09d8d78fc376dca1023dd7" }, "th": { "pin": false, @@ -1631,6 +1721,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1641,7 +1732,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "d61fe3d680aaed5acc82321634d321b6874a7da7" + "revision": "0e6c56bf2ac9f3596e99646e8b8e880b2bde5502" }, "tl": { "pin": false, @@ -1649,6 +1740,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1659,7 +1751,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "899dcf6a227f789cb35873ae55b682cab335c12f" + "revision": "d1c7cd905296829b32e00ed97c16f07216e591a6" }, "tr": { "pin": false, @@ -1667,6 +1759,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1677,7 +1770,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "cecfea20ec91d5f504a69504762e19872e2c121e" + "revision": "1525ed009a4a6f7dfcb4f72b0406d68c8bc9b446" }, "trs": { "pin": false, @@ -1685,6 +1778,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1695,7 +1789,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "4aadf9ce06f6bf87fee51bb2f684cc364d613478" + "revision": "269c1be15683651ee2cac1ad55dea4ad6da43aa6" }, "uk": { "pin": false, @@ -1703,6 +1797,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1713,7 +1808,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "14b20afe5140c0ff57432d1d1336681aa7ccb5ae" + "revision": "99d5ffa0b81e03f66693a61c128237572f6810ee" }, "ur": { "pin": false, @@ -1721,6 +1816,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1731,7 +1827,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "1a556b70e4d31e3af80eea2183a5c2413246a6c9" + "revision": "02ab0cc3169d110862ba0dfed1133d25070d03e3" }, "uz": { "pin": false, @@ -1739,6 +1835,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1749,7 +1846,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "cfe8613db1bc1955a8b0ffdce30f75c61626faa3" + "revision": "8ad4e2f1c365f50e4c79976a94fe364d457c9141" }, "vi": { "pin": false, @@ -1757,6 +1854,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1767,7 +1865,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "e3448088c7029aac37f24f0d4b5efec18a3fd9a9" + "revision": "5fd44724e22de01fe90767faba16be42236b7f7e" }, "xh": { "pin": false, @@ -1775,6 +1873,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1785,7 +1884,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "c9c787a46bb53e078d2f1af90a8cc14fb924da28" + "revision": "802582e42c552f5412369d56855c53bc0ffe0665" }, "zh-CN": { "pin": false, @@ -1793,6 +1892,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1803,7 +1903,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "dbc0842df6bc6d3ca82eedb0da001be16753b6f7" + "revision": "081aeb1aa3080ea25c46b15950cb3257d791ec35" }, "zh-TW": { "pin": false, @@ -1811,6 +1911,7 @@ "linux", "linux-devedition", "linux64", + "linux64-aarch64", "linux64-devedition", "macosx64", "macosx64-devedition", @@ -1821,6 +1922,6 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "6a2e23489a60f5cbbe1f66e68927a7e1dd455525" + "revision": "9015a180602e18b71d4068f6581557542b392344" } }
\ No newline at end of file diff --git a/browser/locales/l10n-onchange-changesets.json b/browser/locales/l10n-onchange-changesets.json index 30c722ebb4..bdabb35e8e 100644 --- a/browser/locales/l10n-onchange-changesets.json +++ b/browser/locales/l10n-onchange-changesets.json @@ -1,6 +1,7 @@ { "ar": { "platforms": [ + "linux64-aarch64", "linux", "linux-devedition", "linux64", @@ -18,6 +19,7 @@ }, "en-CA": { "platforms": [ + "linux64-aarch64", "linux", "linux-devedition", "linux64", @@ -35,6 +37,7 @@ }, "he": { "platforms": [ + "linux64-aarch64", "linux", "linux-devedition", "linux64", @@ -52,6 +55,7 @@ }, "it": { "platforms": [ + "linux64-aarch64", "linux", "linux-devedition", "linux64", @@ -69,6 +73,7 @@ }, "ja": { "platforms": [ + "linux64-aarch64", "linux", "linux-devedition", "linux64", diff --git a/browser/locales/moz.build b/browser/locales/moz.build index 53e3ac361f..66504ba92f 100644 --- a/browser/locales/moz.build +++ b/browser/locales/moz.build @@ -9,9 +9,6 @@ JAR_MANIFESTS += ["jar.mn"] # If DIST_SUBDIR ever gets unset in browser this path might be wrong due to PREF_DIR changing. LOCALIZED_PP_FILES.defaults.preferences += ["en-US/firefox-l10n.js"] -if CONFIG["MOZ_CRASHREPORTER"]: - LOCALIZED_FILES += ["en-US/crashreporter/crashreporter-override.ini"] - if CONFIG["MOZ_UPDATER"]: LOCALIZED_GENERATED_FILES += ["updater.ini"] updater = LOCALIZED_GENERATED_FILES["updater.ini"] diff --git a/browser/modules/AboutNewTab.sys.mjs b/browser/modules/AboutNewTab.sys.mjs index bff8bd3ce4..979c3adf12 100644 --- a/browser/modules/AboutNewTab.sys.mjs +++ b/browser/modules/AboutNewTab.sys.mjs @@ -227,7 +227,7 @@ export const AboutNewTab = { // nsIObserver implementation - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case TOPIC_APP_QUIT: { // We defer to this to the next tick of the event loop since the diff --git a/browser/modules/AsyncTabSwitcher.sys.mjs b/browser/modules/AsyncTabSwitcher.sys.mjs index 4ecdcf7882..9f4aa535e0 100644 --- a/browser/modules/AsyncTabSwitcher.sys.mjs +++ b/browser/modules/AsyncTabSwitcher.sys.mjs @@ -846,7 +846,7 @@ export class AsyncTabSwitcher { // Called when a tab has been removed, and the browser node is // about to be removed from the DOM. - onTabRemovedImpl(tab) { + onTabRemovedImpl() { this.lastVisibleTab = null; } diff --git a/browser/modules/BackgroundTask_install.sys.mjs b/browser/modules/BackgroundTask_install.sys.mjs index 8f13aa8789..510e14bccb 100644 --- a/browser/modules/BackgroundTask_install.sys.mjs +++ b/browser/modules/BackgroundTask_install.sys.mjs @@ -15,7 +15,7 @@ // it. export const backgroundTaskTimeoutSec = 30; -export async function runBackgroundTask(commandLine) { +export async function runBackgroundTask() { console.log("Running BackgroundTask_install."); console.log("Cleaning up update files."); diff --git a/browser/modules/BackgroundTask_uninstall.sys.mjs b/browser/modules/BackgroundTask_uninstall.sys.mjs index 9e51248438..f2fc9a0f8c 100644 --- a/browser/modules/BackgroundTask_uninstall.sys.mjs +++ b/browser/modules/BackgroundTask_uninstall.sys.mjs @@ -12,7 +12,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; -export async function runBackgroundTask(commandLine) { +export async function runBackgroundTask() { console.log("Running BackgroundTask_uninstall."); if (AppConstants.platform === "win") { diff --git a/browser/modules/BrowserUsageTelemetry.sys.mjs b/browser/modules/BrowserUsageTelemetry.sys.mjs index 410d1e2ea3..955a3338ec 100644 --- a/browser/modules/BrowserUsageTelemetry.sys.mjs +++ b/browser/modules/BrowserUsageTelemetry.sys.mjs @@ -1069,7 +1069,7 @@ export let BrowserUsageTelemetry = { this._recordTabCounts({ tabCount, loadedTabCount }); }, - _onTabPinned(target) { + _onTabPinned() { const pinnedTabs = getPinnedTabsCount(); // Update the "tab pinned" count and its maximum. diff --git a/browser/modules/BrowserWindowTracker.sys.mjs b/browser/modules/BrowserWindowTracker.sys.mjs index cead9df7ba..b6e3ad2eea 100644 --- a/browser/modules/BrowserWindowTracker.sys.mjs +++ b/browser/modules/BrowserWindowTracker.sys.mjs @@ -244,7 +244,7 @@ export const BrowserWindowTracker = { // Prevent leaks in case the window closes before we track it as an open // window. const topic = "browsing-context-discarded"; - const observer = (aSubject, aTopic, aData) => { + const observer = aSubject => { if (window.browsingContext == aSubject) { let pending = this.pendingWindows.get(window); if (pending) { diff --git a/browser/modules/ContentCrashHandlers.sys.mjs b/browser/modules/ContentCrashHandlers.sys.mjs index 4ab6f600cd..cd8dfc04c6 100644 --- a/browser/modules/ContentCrashHandlers.sys.mjs +++ b/browser/modules/ContentCrashHandlers.sys.mjs @@ -94,7 +94,7 @@ export var TabCrashHandler = { Services.obs.addObserver(this, "oop-frameloader-crashed"); }, - observe(aSubject, aTopic, aData) { + observe(aSubject, aTopic) { switch (aTopic) { case "ipc:content-shutdown": { aSubject.QueryInterface(Ci.nsIPropertyBag2); @@ -845,7 +845,7 @@ export var UnsubmittedCrashHandler = { Services.obs.removeObserver(this, "profile-before-change"); }, - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "profile-before-change": { this.uninit(); diff --git a/browser/modules/EveryWindow.sys.mjs b/browser/modules/EveryWindow.sys.mjs index 704240b54f..64e7cbaa31 100644 --- a/browser/modules/EveryWindow.sys.mjs +++ b/browser/modules/EveryWindow.sys.mjs @@ -64,7 +64,7 @@ export const EveryWindow = { if (!initialized) { let addUnloadListener = win => { - function observer(subject, topic, data) { + function observer(subject, topic) { if (topic == "domwindowclosed" && subject === win) { Services.ww.unregisterNotification(observer); for (let c of callbacks.values()) { diff --git a/browser/modules/ExtensionsUI.sys.mjs b/browser/modules/ExtensionsUI.sys.mjs index 0b113f87ce..6b2781670a 100644 --- a/browser/modules/ExtensionsUI.sys.mjs +++ b/browser/modules/ExtensionsUI.sys.mjs @@ -185,7 +185,7 @@ export var ExtensionsUI = { ); }, - observe(subject, topic, data) { + observe(subject, topic) { if (topic == "webextension-permission-prompt") { let { target, info } = subject.wrappedJSObject; diff --git a/browser/modules/FaviconLoader.sys.mjs b/browser/modules/FaviconLoader.sys.mjs index 5012ee2c8c..7e28b5d026 100644 --- a/browser/modules/FaviconLoader.sys.mjs +++ b/browser/modules/FaviconLoader.sys.mjs @@ -188,7 +188,7 @@ class FaviconLoad { this.channel.cancel(Cr.NS_BINDING_ABORTED); } - onStartRequest(request) {} + onStartRequest() {} onDataAvailable(request, inputStream, offset, count) { this.stream.writeFrom(inputStream, count); diff --git a/browser/modules/HomePage.sys.mjs b/browser/modules/HomePage.sys.mjs index f8f27d9bd9..612e559e2c 100644 --- a/browser/modules/HomePage.sys.mjs +++ b/browser/modules/HomePage.sys.mjs @@ -308,7 +308,7 @@ export let HomePage = { } }, - onWidgetRemoved(widgetId, area) { + onWidgetRemoved(widgetId) { if (widgetId == kWidgetId) { Services.prefs.setBoolPref(kWidgetRemovedPref, true); lazy.CustomizableUI.removeListener(this); diff --git a/browser/modules/PageActions.sys.mjs b/browser/modules/PageActions.sys.mjs index f5951142dd..2a900c21f0 100644 --- a/browser/modules/PageActions.sys.mjs +++ b/browser/modules/PageActions.sys.mjs @@ -1184,7 +1184,7 @@ PageActions._initBuiltInActions = function () { browserPageActions(buttonNode).bookmark.onShowingInPanel(buttonNode); }, onCommand(event, buttonNode) { - browserPageActions(buttonNode).bookmark.onCommand(event, buttonNode); + browserPageActions(buttonNode).bookmark.onCommand(event); }, }, ]; diff --git a/browser/modules/PermissionUI.sys.mjs b/browser/modules/PermissionUI.sys.mjs index e94beb79ac..b4feb7f0c9 100644 --- a/browser/modules/PermissionUI.sys.mjs +++ b/browser/modules/PermissionUI.sys.mjs @@ -530,7 +530,7 @@ class PermissionPrompt { let action = { label: promptAction.label, accessKey: promptAction.accessKey, - callback: state => { + callback: () => { if (promptAction.callback) { promptAction.callback(); } @@ -725,7 +725,7 @@ class SitePermsAddonInstallRequest extends PermissionPromptForRequest { * @param {Components.Exception} err * @returns {String} The error message */ - getInstallErrorMessage(err) { + getInstallErrorMessage() { return null; } } @@ -1397,7 +1397,7 @@ class StorageAccessPermissionPrompt extends PermissionPromptForRequest { "storageAccess1.Allow.accesskey" ), action: Ci.nsIPermissionManager.ALLOW_ACTION, - callback(state) { + callback() { self.allow({ "storage-access": "allow" }); }, }, @@ -1409,7 +1409,7 @@ class StorageAccessPermissionPrompt extends PermissionPromptForRequest { "storageAccess1.DontAllow.accesskey" ), action: Ci.nsIPermissionManager.DENY_ACTION, - callback(state) { + callback() { self.cancel(); }, }, diff --git a/browser/modules/ProcessHangMonitor.sys.mjs b/browser/modules/ProcessHangMonitor.sys.mjs index f0939449c9..e0f91cdf93 100644 --- a/browser/modules/ProcessHangMonitor.sys.mjs +++ b/browser/modules/ProcessHangMonitor.sys.mjs @@ -210,7 +210,7 @@ export var ProcessHangMonitor = { return func(report); }, - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "xpcom-shutdown": { Services.obs.removeObserver(this, "xpcom-shutdown"); @@ -240,7 +240,7 @@ export var ProcessHangMonitor = { // Install event listeners on the new window in case one of // its tabs is already hung. let win = subject; - let listener = ev => { + let listener = () => { win.removeEventListener("load", listener, true); this.updateWindows(); }; @@ -548,10 +548,11 @@ export var ProcessHangMonitor = { return; } - // Show the "debug script" button unconditionally if we are in Developer edition, - // or, if DevTools are opened on the slow tab. + // Show the "debug script" button unconditionally if we are in Developer or Nightly + // editions, or if DevTools are opened on the slow tab. if ( AppConstants.MOZ_DEV_EDITION || + AppConstants.NIGHTLY_BUILD || report.scriptBrowser.browsingContext.watchedByDevTools ) { buttons.push({ diff --git a/browser/modules/Sanitizer.sys.mjs b/browser/modules/Sanitizer.sys.mjs index 13b7a307ea..3b0f960d18 100644 --- a/browser/modules/Sanitizer.sys.mjs +++ b/browser/modules/Sanitizer.sys.mjs @@ -24,7 +24,7 @@ XPCOMUtils.defineLazyPreferenceGetter( ); var logConsole; -function log(msg) { +function log(...msgs) { if (!logConsole) { logConsole = console.createInstance({ prefix: "Sanitizer", @@ -32,7 +32,7 @@ function log(msg) { }); } - logConsole.log(msg); + logConsole.log(...msgs); } // Used as unique id for pending sanitizations. @@ -164,6 +164,7 @@ export var Sanitizer = { // First, collect pending sanitizations from the last session, before we // add pending sanitizations for this session. let pendingSanitizations = getAndClearPendingSanitizations(); + log("Pending sanitizations:", pendingSanitizations); // Check if we should sanitize on shutdown. this.shouldSanitizeOnShutdown = Services.prefs.getBoolPref( @@ -759,7 +760,7 @@ export var Sanitizer = { // closes) and/or run too late (and not have a fully-formed window yet // in existence). See bug 1088137. let newWindowOpened = false; - let onWindowOpened = function (subject, topic, data) { + let onWindowOpened = function (subject) { if (subject != newWindow) { return; } @@ -811,7 +812,7 @@ export var Sanitizer = { }, pluginData: { - async clear(range) {}, + async clear() {}, }, // Combine History and Form Data clearing for the @@ -923,25 +924,13 @@ export var Sanitizer = { await maybeSanitizeSessionPrincipals( progress, principalsForShutdownClearing, - Ci.nsIClearDataService.CLEAR_COOKIES | - Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD | - Ci.nsIClearDataService.CLEAR_DOM_STORAGES | - Ci.nsIClearDataService.CLEAR_AUTH_TOKENS | - Ci.nsIClearDataService.CLEAR_AUTH_CACHE | - Ci.nsIClearDataService.CLEAR_FINGERPRINTING_PROTECTION_STATE | - Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE + Ci.nsIClearDataService.CLEAR_COOKIES_AND_SITE_DATA ); } else { // Not on shutdown await clearData( range, - Ci.nsIClearDataService.CLEAR_COOKIES | - Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD | - Ci.nsIClearDataService.CLEAR_DOM_STORAGES | - Ci.nsIClearDataService.CLEAR_AUTH_TOKENS | - Ci.nsIClearDataService.CLEAR_AUTH_CACHE | - Ci.nsIClearDataService.CLEAR_FINGERPRINTING_PROTECTION_STATE | - Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE + Ci.nsIClearDataService.CLEAR_COOKIES_AND_SITE_DATA ); } await clearData(range, Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES); @@ -1018,6 +1007,7 @@ async function sanitizeInternal(items, aItemsToClear, options) { // Array of objects in form { name, promise }. // `name` is the item's name and `promise` may be a promise, if the // sanitization is asynchronous, or the function return value, otherwise. + log("Running sanitization for:", itemsToClear); let handles = []; for (let name of itemsToClear) { progress[name] = "blocking"; @@ -1046,7 +1036,7 @@ async function sanitizeInternal(items, aItemsToClear, options) { } await Promise.all(handles.map(h => h.promise)); - // Sanitization is complete. + log("All sanitizations are complete"); TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj); if (!progress.isShutdown) { removePendingSanitization(uid); diff --git a/browser/modules/SiteDataManager.sys.mjs b/browser/modules/SiteDataManager.sys.mjs index c5569afc82..082d250ddc 100644 --- a/browser/modules/SiteDataManager.sys.mjs +++ b/browser/modules/SiteDataManager.sys.mjs @@ -504,23 +504,6 @@ export var SiteDataManager = { site.cookies = []; }, - // Returns a list of permissions from the permission manager that - // we consider part of "site data and cookies". - _getDeletablePermissions() { - let perms = []; - - for (let permission of Services.perms.all) { - if ( - permission.type == "persistent-storage" || - permission.type == "storage-access" - ) { - perms.push(permission); - } - } - - return perms; - }, - /** * Removes all site data for the specified list of domains and hosts. * This includes site data of subdomains belonging to the domains or hosts and @@ -542,7 +525,7 @@ export var SiteDataManager = { if (!Array.isArray(domainsOrHosts)) { domainsOrHosts = [domainsOrHosts]; } - let perms = this._getDeletablePermissions(); + let promises = []; for (let domainOrHost of domainsOrHosts) { const kFlags = @@ -552,7 +535,8 @@ export var SiteDataManager = { Ci.nsIClearDataService.CLEAR_ALL_CACHES | Ci.nsIClearDataService.CLEAR_COOKIE_BANNER_EXECUTED_RECORD | Ci.nsIClearDataService.CLEAR_FINGERPRINTING_PROTECTION_STATE | - Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE; + Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE | + Ci.nsIClearDataService.CLEAR_STORAGE_PERMISSIONS; promises.push( new Promise(function (resolve) { const { clearData } = Services; @@ -580,19 +564,6 @@ export var SiteDataManager = { } }) ); - - for (let perm of perms) { - // Specialcase local file permissions. - if (!domainOrHost) { - if (perm.principal.schemeIs("file")) { - Services.perms.removePermission(perm); - } - } else if ( - Services.eTLD.hasRootDomain(perm.principal.host, domainOrHost) - ) { - Services.perms.removePermission(perm); - } - } } await Promise.all(promises); @@ -687,19 +658,11 @@ export var SiteDataManager = { async removeSiteData() { await new Promise(function (resolve) { Services.clearData.deleteData( - Ci.nsIClearDataService.CLEAR_COOKIES | - Ci.nsIClearDataService.CLEAR_DOM_STORAGES | - Ci.nsIClearDataService.CLEAR_HSTS | - Ci.nsIClearDataService.CLEAR_EME | - Ci.nsIClearDataService.CLEAR_BOUNCE_TRACKING_PROTECTION_STATE, + Ci.nsIClearDataService.CLEAR_COOKIES_AND_SITE_DATA, resolve ); }); - for (let permission of this._getDeletablePermissions()) { - Services.perms.removePermission(permission); - } - return this.updateSites(); }, }; diff --git a/browser/modules/SitePermissions.sys.mjs b/browser/modules/SitePermissions.sys.mjs index 2f3f9210e2..8e1aa77871 100644 --- a/browser/modules/SitePermissions.sys.mjs +++ b/browser/modules/SitePermissions.sys.mjs @@ -630,7 +630,7 @@ export var SitePermissions = { * @param {string} latest * The latest value of the preference */ - invalidatePermissionList(data, previous, latest) { + invalidatePermissionList() { // Ensure that listPermissions() will reconstruct its return value the next // time it's called. this._permissionsArray = null; diff --git a/browser/modules/TabUnloader.sys.mjs b/browser/modules/TabUnloader.sys.mjs index a0c1233f27..2bd6097b5d 100644 --- a/browser/modules/TabUnloader.sys.mjs +++ b/browser/modules/TabUnloader.sys.mjs @@ -63,7 +63,7 @@ let DefaultTabUnloaderMethods = { return tab.pinned ? weight : 0; }, - isLoading(tab, weight) { + isLoading() { return 0; }, diff --git a/browser/modules/TabsList.sys.mjs b/browser/modules/TabsList.sys.mjs index 9e6c36f4a8..44878afb8f 100644 --- a/browser/modules/TabsList.sys.mjs +++ b/browser/modules/TabsList.sys.mjs @@ -100,7 +100,7 @@ class TabsListBase { /* * Populate the popup with menuitems and setup the listeners. */ - _populate(event) { + _populate() { let fragment = this.doc.createDocumentFragment(); for (let tab of this.gBrowser.tabs) { diff --git a/browser/modules/WindowsJumpLists.sys.mjs b/browser/modules/WindowsJumpLists.sys.mjs index 9015527423..a4493fc591 100644 --- a/browser/modules/WindowsJumpLists.sys.mjs +++ b/browser/modules/WindowsJumpLists.sys.mjs @@ -560,7 +560,7 @@ var Builder = class { aError.message ); }, - handleCompletion(aReason) { + handleCompletion() { aCallback.call(aScope, null); }, }); @@ -815,7 +815,7 @@ export var WinTaskbarJumpList = { name: "WinTaskbarJumpList", - notify: function WTBJL_notify(aTimer) { + notify: function WTBJL_notify() { // Add idle observer on the first notification so it doesn't hit startup. this._updateIdleObserver(); Services.tm.idleDispatchToMainThread(() => { @@ -823,7 +823,7 @@ export var WinTaskbarJumpList = { }); }, - observe: function WTBJL_observe(aSubject, aTopic, aData) { + observe: function WTBJL_observe(aSubject, aTopic) { switch (aTopic) { case "nsPref:changed": if (this._enabled && !lazy._prefs.getBoolPref(PREF_TASKBAR_ENABLED)) { diff --git a/browser/modules/WindowsPreviewPerTab.sys.mjs b/browser/modules/WindowsPreviewPerTab.sys.mjs index c0ef87fe8b..1dd226ec02 100644 --- a/browser/modules/WindowsPreviewPerTab.sys.mjs +++ b/browser/modules/WindowsPreviewPerTab.sys.mjs @@ -88,7 +88,7 @@ function _imageFromURI(uri, privateMode, callback) { } const decodeCallback = { - onImageReady(image, status) { + onImageReady(image) { if (!image) { // We failed, so use the default favicon (only if this wasn't the // default favicon). @@ -589,13 +589,13 @@ TabWindow.prototype = { // Browser progress listener - onLocationChange(aBrowser) { + onLocationChange() { // I'm not sure we need this, onStateChange does a really good job // of picking up page changes. // this.invalidateTabPreview(aBrowser); }, - onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags) { if ( aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK diff --git a/browser/modules/ZoomUI.sys.mjs b/browser/modules/ZoomUI.sys.mjs index 2e32ba46ec..d0ff9c7188 100644 --- a/browser/modules/ZoomUI.sys.mjs +++ b/browser/modules/ZoomUI.sys.mjs @@ -64,7 +64,7 @@ export var ZoomUI = { value = parseFloat(pref.value); } }, - handleCompletion(reason) { + handleCompletion() { resolve(value); }, handleError(error) { @@ -75,7 +75,7 @@ export var ZoomUI = { }, }; -function fullZoomLocationChangeObserver(aSubject, aTopic) { +function fullZoomLocationChangeObserver(aSubject) { // If the tab was the last one in its window and has been dragged to another // window, the original browser's window will be unavailable here. Since that // window is closing, we can just ignore this notification. diff --git a/browser/modules/test/browser/browser_PageActions.js b/browser/modules/test/browser/browser_PageActions.js index 4f86962a01..c40bfd97d7 100644 --- a/browser/modules/test/browser/browser_PageActions.js +++ b/browser/modules/test/browser/browser_PageActions.js @@ -578,7 +578,7 @@ add_task(async function withIframe() { pinnedToUrlbar: true, title: "Test iframe", wantsIframe: true, - onCommand(event, buttonNode) { + onCommand() { onCommandCallCount++; }, onIframeShowing(iframeNode, panelNode) { @@ -1171,10 +1171,10 @@ add_task(async function transient() { id: "test-transient", title: "Test transient", _transient: true, - onPlacedInPanel(buttonNode) { + onPlacedInPanel() { onPlacedInPanelCount++; }, - onBeforePlacedInWindow(win) { + onBeforePlacedInWindow() { onBeforePlacedInWindowCount++; }, }) diff --git a/browser/modules/test/browser/browser_ProcessHangNotifications.js b/browser/modules/test/browser/browser_ProcessHangNotifications.js index d176f911ef..9150c36d4c 100644 --- a/browser/modules/test/browser/browser_ProcessHangNotifications.js +++ b/browser/modules/test/browser/browser_ProcessHangNotifications.js @@ -2,7 +2,7 @@ const { WebExtensionPolicy } = Cu.getGlobalForObject(Services); -function promiseNotificationShown(aWindow, aName) { +function promiseNotificationShown(aWindow) { return new Promise(resolve => { let notificationBox = aWindow.gNotificationBox; notificationBox.stack.addEventListener( @@ -52,7 +52,7 @@ let TestHangReport = function ( hangType = SLOW_SCRIPT, browser = gBrowser.selectedBrowser ) { - this.promise = new Promise((resolve, reject) => { + this.promise = new Promise(resolve => { this._resolver = resolve; }); @@ -98,7 +98,8 @@ TestHangReport.prototype = { }; // on dev edition we add a button for js debugging of hung scripts. -let buttonCount = AppConstants.MOZ_DEV_EDITION ? 2 : 1; +let buttonCount = + AppConstants.MOZ_DEV_EDITION || AppConstants.NIGHTLY_BUILD ? 2 : 1; add_setup(async function () { // Create a fake WebExtensionPolicy that we can use for diff --git a/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js b/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js index 4305d7f7df..6300bd17ba 100644 --- a/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js +++ b/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js @@ -98,7 +98,7 @@ function createPendingCrashReports(howMany, accessDate) { }; let uuidGenerator = Services.uuid; - // Some annotations are always present in the .extra file and CrashSubmit.jsm + // Some annotations are always present in the .extra file and CrashSubmit.sys.mjs // expects there to be a ServerURL entry, so we'll add them here. let extraFileContents = JSON.stringify({ ServerURL: SERVER_URL, diff --git a/browser/modules/test/browser/browser_UsageTelemetry_interaction.js b/browser/modules/test/browser/browser_UsageTelemetry_interaction.js index 2bc60d9697..5fa436a349 100644 --- a/browser/modules/test/browser/browser_UsageTelemetry_interaction.js +++ b/browser/modules/test/browser/browser_UsageTelemetry_interaction.js @@ -316,7 +316,7 @@ add_task(async function contextMenu_entrypoints() { }); add_task(async function appMenu() { - await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await BrowserTestUtils.withNewTab("https://example.com", async () => { Services.telemetry.getSnapshotForKeyedScalars("main", true); let shown = BrowserTestUtils.waitForEvent( @@ -348,7 +348,7 @@ add_task(async function appMenu() { }); add_task(async function devtools() { - await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await BrowserTestUtils.withNewTab("https://example.com", async () => { Services.telemetry.getSnapshotForKeyedScalars("main", true); let shown = BrowserTestUtils.waitForEvent( @@ -667,7 +667,7 @@ add_task(async function mainMenu() { BrowserUsageTelemetry._resetAddonIds(); - await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await BrowserTestUtils.withNewTab("https://example.com", async () => { Services.telemetry.getSnapshotForKeyedScalars("main", true); CustomizableUI.setToolbarVisibility("toolbar-menubar", true); @@ -702,7 +702,7 @@ add_task(async function preferences() { ? "sync-pane-loaded" : "privacy-pane-loaded"; let finalPrefPaneLoaded = TestUtils.topicObserved(finalPaneEvent, () => true); - await BrowserTestUtils.withNewTab("about:preferences", async browser => { + await BrowserTestUtils.withNewTab("about:preferences", async () => { await finalPrefPaneLoaded; Services.telemetry.getSnapshotForKeyedScalars("main", true); @@ -778,7 +778,7 @@ async function openLinkUsingContextMenu(link) { } async function history_appMenu(useContextClick) { - await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await BrowserTestUtils.withNewTab("https://example.com", async () => { let shown = BrowserTestUtils.waitForEvent( elem("appMenu-popup"), "popupshown" @@ -819,7 +819,7 @@ add_task(async function history_appMenu_context_click() { }); async function bookmarks_appMenu(useContextClick) { - await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await BrowserTestUtils.withNewTab("https://example.com", async () => { let shown = BrowserTestUtils.waitForEvent( elem("appMenu-popup"), "popupshown" @@ -865,7 +865,7 @@ add_task(async function bookmarks_appMenu_context_click() { }); async function bookmarks_library_navbar(useContextClick) { - await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await BrowserTestUtils.withNewTab("https://example.com", async () => { CustomizableUI.addWidgetToArea("library-button", "nav-bar"); let button = document.getElementById("library-button"); button.click(); @@ -908,7 +908,7 @@ add_task(async function bookmarks_library_navbar_context_click() { }); async function history_library_navbar(useContextClick) { - await BrowserTestUtils.withNewTab("https://example.com", async browser => { + await BrowserTestUtils.withNewTab("https://example.com", async () => { CustomizableUI.addWidgetToArea("library-button", "nav-bar"); let button = document.getElementById("library-button"); button.click(); diff --git a/browser/modules/test/browser/browser_UsageTelemetry_private_and_restore.js b/browser/modules/test/browser/browser_UsageTelemetry_private_and_restore.js index 89222739be..94447c69ae 100644 --- a/browser/modules/test/browser/browser_UsageTelemetry_private_and_restore.js +++ b/browser/modules/test/browser/browser_UsageTelemetry_private_and_restore.js @@ -19,7 +19,7 @@ registerCleanupFunction(() => { function promiseBrowserStateRestored() { return new Promise(resolve => { - Services.obs.addObserver(function observer(aSubject, aTopic) { + Services.obs.addObserver(function observer() { Services.obs.removeObserver( observer, "sessionstore-browser-state-restored" diff --git a/browser/modules/test/browser/browser_preloading_tab_moving.js b/browser/modules/test/browser/browser_preloading_tab_moving.js index ce7cba9e85..1118657980 100644 --- a/browser/modules/test/browser/browser_preloading_tab_moving.js +++ b/browser/modules/test/browser/browser_preloading_tab_moving.js @@ -27,7 +27,7 @@ async function promiseNewTabLoadedInBrowser(browser) { info(`Waiting for ${url} to be the location for the browser.`); await new Promise(resolve => { let progressListener = { - onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) { + onLocationChange(aWebProgress, aRequest, aLocationURI) { if (!url || aLocationURI.spec == url) { browser.removeProgressListener(progressListener); resolve(); diff --git a/browser/modules/test/browser/formValidation/browser_form_validation.js b/browser/modules/test/browser/formValidation/browser_form_validation.js index 6348546c80..8e0584269f 100644 --- a/browser/modules/test/browser/formValidation/browser_form_validation.js +++ b/browser/modules/test/browser/formValidation/browser_form_validation.js @@ -161,7 +161,7 @@ add_task(async function () { await clickChildElement(browser); - await new Promise((resolve, reject) => { + await new Promise(resolve => { // XXXndeakin This isn't really going to work when the content is another process executeSoon(function () { checkPopupHide(); @@ -289,7 +289,7 @@ add_task(async function () { gInvalidFormPopup.firstElementChild.textContent ); - await new Promise((resolve, reject) => { + await new Promise(resolve => { EventUtils.sendString("a"); executeSoon(function () { checkPopupShow(anchorRect); @@ -475,7 +475,7 @@ add_task(async function () { // Now, the element suffers from another error, the message should have // been updated. - await new Promise((resolve, reject) => { + await new Promise(resolve => { // XXXndeakin This isn't really going to work when the content is another process executeSoon(function () { checkChildFocus(browser, gInvalidFormPopup.firstElementChild.textContent); @@ -515,7 +515,7 @@ add_task(async function () { gInvalidFormPopup, "popuphidden" ); - BrowserReloadSkipCache(); + BrowserCommands.reloadSkipCache(); await popupHiddenPromise; gBrowser.removeCurrentTab(); diff --git a/browser/modules/test/browser/head.js b/browser/modules/test/browser/head.js index f852cdd641..da82fa1c39 100644 --- a/browser/modules/test/browser/head.js +++ b/browser/modules/test/browser/head.js @@ -95,7 +95,7 @@ function makeMockPermissionRequest(browser) { allow() { this._allowed = true; }, - getDelegatePrincipal(aType) { + getDelegatePrincipal() { return principal; }, QueryInterface: ChromeUtils.generateQI(["nsIContentPermissionRequest"]), diff --git a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js index 1273ee950b..77db0d8286 100644 --- a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js +++ b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js @@ -39,7 +39,7 @@ class StubbedRegistryKey { this.#children = new Map(this.#originalChildren); } - open(accessLevel) { + open(_accessLevel) { this.#openedForRead = true; } diff --git a/browser/modules/webrtcUI.sys.mjs b/browser/modules/webrtcUI.sys.mjs index 384cec16af..f270f89d77 100644 --- a/browser/modules/webrtcUI.sys.mjs +++ b/browser/modules/webrtcUI.sys.mjs @@ -112,7 +112,7 @@ export var webrtcUI = { } }, - observe(subject, topic, data) { + observe(subject, topic) { if (topic == "browser-delayed-startup-finished") { if (webrtcUI.showGlobalIndicator) { showOrCreateMenuForWindow(subject); @@ -1106,7 +1106,7 @@ function onTabSharingMenuPopupShowing(e) { } } -function onTabSharingMenuPopupHiding(e) { +function onTabSharingMenuPopupHiding() { while (this.lastChild) { this.lastChild.remove(); } diff --git a/browser/moz.build b/browser/moz.build index fdcef15ede..7d8b991e46 100644 --- a/browser/moz.build +++ b/browser/moz.build @@ -63,7 +63,7 @@ FINAL_TARGET_FILES.defaults += ["app/permissions"] with Files("**"): BUG_COMPONENT = ("Firefox", "General") - SCHEDULES.exclusive = ["linux", "macosx", "windows"] + SCHEDULES.exclusive = ["linux", "macosx", "windows", "firefox"] with Files("docs/**"): SCHEDULES.exclusive = ["docs"] diff --git a/browser/themes/BuiltInThemes.sys.mjs b/browser/themes/BuiltInThemes.sys.mjs index c2d5dd7a18..c6b9958a2a 100644 --- a/browser/themes/BuiltInThemes.sys.mjs +++ b/browser/themes/BuiltInThemes.sys.mjs @@ -277,8 +277,8 @@ class _BuiltInThemes { * there's none. */ getColorwayIntensityL10nId(colorwayId) { - const result = ColorwayIntensityIdPostfixToL10nMap.find( - ([postfix, l10nId]) => colorwayId.endsWith(postfix) + const result = ColorwayIntensityIdPostfixToL10nMap.find(([postfix]) => + colorwayId.endsWith(postfix) ); return result ? result[1] : null; } diff --git a/browser/themes/ThemeVariableMap.sys.mjs b/browser/themes/ThemeVariableMap.sys.mjs index c0c9042efc..1795bfe7c1 100644 --- a/browser/themes/ThemeVariableMap.sys.mjs +++ b/browser/themes/ThemeVariableMap.sys.mjs @@ -56,12 +56,22 @@ export const ThemeVariableMap = [ }, ], [ - "--tabs-navbar-shadow-color", + "--tabs-navbar-separator-color", { lwtProperty: "toolbar_top_separator", }, ], [ + "--tabs-navbar-separator-style", + { + lwtProperty: "toolbar_top_separator", + processColor(rgbaChannels) { + // If the separator is transparent, we don't want it to take space. + return rgbaChannels?.a === 0 ? "none" : null; + }, + }, + ], + [ "--toolbarseparator-color", { lwtProperty: "toolbar_vertical_separator", diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css index 89df26a2f0..963a33af85 100644 --- a/browser/themes/linux/browser.css +++ b/browser/themes/linux/browser.css @@ -16,7 +16,7 @@ * disabling the toolbar field border and backgrounds. */ @media not (prefers-contrast) { - :root:not(:-moz-lwtheme) { + :root:not([lwtheme]) { --toolbar-field-border-color: transparent; /* These colors don't exactly match the default dark color that buttons and @@ -36,7 +36,7 @@ --toolbar-field-color: inherit; @media (-moz-gtk-theme-family) { - --tabs-navbar-shadow-color: transparent; + --tabs-navbar-separator-style: none; @media (prefers-color-scheme: light) { --urlbar-box-bgcolor: #fafafa; } @@ -237,7 +237,7 @@ @media (-moz-bool-pref: "widget.gtk.non-native-titlebar-buttons.enabled") { /* When using lightweight themes, use our own buttons since native ones might * assume a native background in order to be visible. */ - &:-moz-lwtheme { + :root[lwtheme] & { padding-inline: 3px; > .toolbarbutton-icon { diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index b62710cc71..8ed2ec042d 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -3,12 +3,54 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ @import url("chrome://browser/skin/browser-shared.css"); -@import url("chrome://browser/skin/browser-custom-colors.css"); - -@namespace html url("http://www.w3.org/1999/xhtml"); +/* stylelint-disable-next-line media-query-no-invalid */ +@import url("chrome://browser/skin/browser-custom-colors.css") not (-moz-bool-pref: "browser.theme.macos.native-theme"); :root { --arrowpanel-field-background: light-dark(rgba(249, 249, 250, .3), rgba(12, 12, 13, .3)); + + /* On macOS, top level windows are always opaque. This gives us the right + * default background color, without confusing Gecko about whether the window + * is transparent or not. */ + appearance: auto; + -moz-default-appearance: -moz-mac-unified-toolbar-window; +} + +/* stylelint-disable-next-line media-query-no-invalid */ +@media (-moz-bool-pref: "browser.theme.macos.native-theme") { + /* TODO: Share this with Linux, which effectively does ~the same */ + @media not (prefers-contrast) { + :root:not([lwtheme]) { + --toolbar-field-border-color: transparent; + --toolbar-field-background-color: light-dark(rgba(0, 0, 0, .05), rgba(0, 0, 0, .3)); + --toolbar-field-color: inherit; + @media (prefers-color-scheme: light) { + --toolbar-non-lwt-bgcolor: white; + --urlbar-box-bgcolor: #fafafa; + } + @media (prefers-color-scheme: dark) { + --tab-selected-bgcolor: color-mix(in srgb, -moz-dialog 75%, white); + } + } + } + + /* Don't make the toolbox vibrant when in full-screen. macOS fullscreen has a + * native titlebar outside of the window (revealed on hover) anyways. */ + :root[tabsintitlebar]:not([lwtheme], [macOSNativeFullscreen]) #navigator-toolbox { + background-color: transparent; + + /* This is conceptually a background, but putting this on a + * pseudo-element avoids it from suppressing the chrome-content separator + * border, etc */ + &::after { + -moz-default-appearance: -moz-window-titlebar; + appearance: auto; + content: ""; + position: absolute; + inset: 0; + z-index: -1; + } + } } #browser, @@ -231,7 +273,7 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left { text-shadow: inherit; @media (prefers-color-scheme: light) { - &:not(:-moz-lwtheme) { + :root:not([lwtheme]) & { /* overriding tabbox.css */ color: hsl(240, 5%, 5%); } diff --git a/browser/themes/osx/customizableui/panelUI.css b/browser/themes/osx/customizableui/panelUI.css index ceb22f7224..2a4508cb05 100644 --- a/browser/themes/osx/customizableui/panelUI.css +++ b/browser/themes/osx/customizableui/panelUI.css @@ -8,10 +8,6 @@ scrollbar-color: color-mix(in srgb, currentColor 26%, transparent) transparent; } -#appMenu-mainView > .panel-subview-body > .panel-banner-item { - padding-inline-start: 18px; -} - .subviewbutton:not([image],[targetURI],.bookmark-item) > .menu-iconic-left { display: none; } diff --git a/browser/themes/osx/places/organizer.css b/browser/themes/osx/places/organizer.css index 61e450a345..fc2698aff1 100644 --- a/browser/themes/osx/places/organizer.css +++ b/browser/themes/osx/places/organizer.css @@ -7,6 +7,63 @@ appearance: auto; } +#placesToolbar { + position: relative; + -moz-window-dragging: drag; + padding: env(-moz-mac-titlebar-height) 4px 3px; + border-bottom: 1px solid ThreeDShadow; + + &::after { + content: ""; + position: absolute; + inset: 0; + appearance: auto; + -moz-default-appearance: -moz-window-titlebar; + z-index: -1; + } + + > toolbarbutton { + margin: 4px 4px 5px; + padding: 0; + height: 22px; + appearance: auto; + -moz-default-appearance: toolbarbutton; + + > .toolbarbutton-icon { + -moz-context-properties: fill, fill-opacity; + fill: currentColor; + fill-opacity: 0.8; + margin: 1px 4px; + } + + &:not(#clearDownloadsButton) > .toolbarbutton-text { + display: none; + } + + &[type="menu"] > .toolbarbutton-menu-dropmarker { + content: url(chrome://global/skin/icons/arrow-down-12.svg); + padding: 0; + margin-inline-end: 2px; + } + + &[disabled] > .toolbarbutton-icon, + &:not(:hover):-moz-window-inactive > .toolbarbutton-icon, + &[type="menu"][disabled] > .toolbarbutton-menu-dropmarker, + &:not(:hover):-moz-window-inactive[type="menu"] > .toolbarbutton-menu-dropmarker { + opacity: .5; + } + + &:-moz-window-inactive[disabled] > .toolbarbutton-icon, + &:-moz-window-inactive[type="menu"][disabled] > .toolbarbutton-menu-dropmarker { + opacity: .25; + } + + > menupopup { + margin-top: 1px; + } + } +} + /* Places Organizer Sidebars */ #placesList { @@ -14,78 +71,33 @@ width: 160px; min-width: 100px; max-width: 400px; -} - -#placesList > treechildren::-moz-tree-cell-text { - margin-inline-end: 6px; -} -#placesList > treechildren::-moz-tree-cell(separator) { - cursor: default; -} + > treechildren::-moz-tree-cell-text { + margin-inline-end: 6px; + } -#placesList > treechildren::-moz-tree-separator { - border-top: 1px solid color-mix(in srgb, FieldText 70%, transparent); - margin: 0 10px; -} + > treechildren::-moz-tree-cell(separator) { + cursor: default; + } -#placesToolbar { - padding: 0 4px 3px; + > treechildren::-moz-tree-separator { + border-top: 1px solid color-mix(in srgb, FieldText 70%, transparent); + margin: 0 10px; + } } #placesView { border-top: none !important; -} - -#placesView > splitter { - border-inline-start: none !important; - border-inline-end: 1px solid color-mix(in srgb, FieldText 30%, transparent); - min-width: 1px; - width: 3px; - margin-inline-start: -3px; - position: relative; - background-image: none !important; -} - -#placesToolbar > toolbarbutton { - margin: 4px 4px 5px; - padding: 0; - height: 22px; - appearance: auto; - -moz-default-appearance: toolbarbutton; -} - -#placesToolbar > toolbarbutton > .toolbarbutton-icon { - -moz-context-properties: fill, fill-opacity; - fill: currentColor; - fill-opacity: 0.8; - margin: 1px 4px; -} - -#placesToolbar > toolbarbutton:not(#clearDownloadsButton) > .toolbarbutton-text { - display: none; -} - -#placesToolbar > toolbarbutton[type="menu"] > .toolbarbutton-menu-dropmarker { - content: url(chrome://global/skin/icons/arrow-down-12.svg); - padding: 0; - margin-inline-end: 2px; -} - -#placesToolbar > toolbarbutton[disabled] > .toolbarbutton-icon, -#placesToolbar > toolbarbutton:not(:hover):-moz-window-inactive > .toolbarbutton-icon, -#placesToolbar > toolbarbutton[type="menu"][disabled] > .toolbarbutton-menu-dropmarker, -#placesToolbar > toolbarbutton:not(:hover):-moz-window-inactive[type="menu"] > .toolbarbutton-menu-dropmarker { - opacity: .5; -} - -#placesToolbar > toolbarbutton:-moz-window-inactive[disabled] > .toolbarbutton-icon, -#placesToolbar > toolbarbutton:-moz-window-inactive[type="menu"][disabled] > .toolbarbutton-menu-dropmarker { - opacity: .25; -} -#placesToolbar > toolbarbutton > menupopup { - margin-top: 1px; + > splitter { + border-inline-start: none !important; + border-inline-end: 1px solid color-mix(in srgb, FieldText 30%, transparent); + min-width: 1px; + width: 3px; + margin-inline-start: -3px; + position: relative; + background-image: none !important; + } } /* back and forward button */ diff --git a/browser/themes/shared/addons/unified-extensions.css b/browser/themes/shared/addons/unified-extensions.css index fd671e007d..06afeec3a7 100644 --- a/browser/themes/shared/addons/unified-extensions.css +++ b/browser/themes/shared/addons/unified-extensions.css @@ -10,7 +10,7 @@ --uei-button-hover-color: inherit; --uei-button-active-bgcolor: var(--panel-item-active-bgcolor); --uei-button-active-color: inherit; - --uei-button-attention-dot-color: var(--tab-attention-icon-color); + --uei-button-attention-dot-color: var(--attention-dot-color); } :root[uidensity="compact"] { @@ -179,56 +179,52 @@ unified-extensions-item > .subviewbutton { /* --- browser action CUI widget styles in the extensions panel --- */ @media (prefers-contrast) { - :root:not(:-moz-lwtheme) { + :root:not([lwtheme]) { --uei-button-attention-dot-color: ButtonText; - } - - .unified-extensions-item-action-button.subviewbutton:not([disabled], :-moz-lwtheme), - .unified-extensions-item-menu-button.subviewbutton > .toolbarbutton-icon:not(:-moz-lwtheme) { - border-color: currentColor; - background-color: ButtonFace; - color: ButtonText; - --uei-button-hover-bgcolor: SelectedItem; - --uei-button-hover-color: SelectedItemText; - --uei-button-active-bgcolor: ButtonFace; - --uei-button-active-color: ButtonText; - } - - .unified-extensions-item-action-button[disabled].subviewbutton:not(:-moz-lwtheme) { - background-color: Canvas; - color: GrayText !important; /* override panelUI-shared.css */ - opacity: 1 !important; /* override panelUI-shared.css */ - } - - .unified-extensions-item[attention] > .unified-extensions-item-action-button.subviewbutton:hover:not(:-moz-lwtheme) { - --uei-button-attention-dot-color: ButtonFace; - } - - .unified-extensions-item[attention] > .unified-extensions-item-action-button.subviewbutton:hover:active:not(:-moz-lwtheme) { - --uei-button-attention-dot-color: ButtonText; - } - - .unified-extensions-item-message:not(:-moz-lwtheme) { - color: inherit; - } - - .unified-extensions-item > .unified-extensions-item-action-button.subviewbutton:hover:not([disabled], :-moz-lwtheme), - .unified-extensions-item > .unified-extensions-item-menu-button.subviewbutton:hover > .toolbarbutton-icon:not(:-moz-lwtheme) { - background-color: var(--uei-button-hover-bgcolor); - color: var(--uei-button-hover-color); - border-color: var(--uei-button-hover-bgcolor); - } - - .unified-extensions-item > .unified-extensions-item-action-button.subviewbutton:hover:active:not([disabled], :-moz-lwtheme), - .unified-extensions-item > .unified-extensions-item-menu-button.subviewbutton:hover:active > .toolbarbutton-icon:not(:-moz-lwtheme) { - background-color: var(--uei-button-active-bgcolor); - color: var(--uei-button-active-color); - border-color: var(--uei-button-active-color); - } - .unified-extensions-item > .unified-extensions-item-menu-button.subviewbutton:focus-visible > .toolbarbutton-icon:not(:-moz-lwtheme) { - /* The border would otherwise overlap with the focus outline, causing an - * unsightly anti-aliasing artifact */ - border-color: transparent; + .unified-extensions-item-action-button.subviewbutton:not([disabled]), + .unified-extensions-item-menu-button.subviewbutton > .toolbarbutton-icon { + border-color: currentColor; + background-color: ButtonFace; + color: ButtonText; + --uei-button-hover-bgcolor: SelectedItem; + --uei-button-hover-color: SelectedItemText; + --uei-button-active-bgcolor: ButtonFace; + --uei-button-active-color: ButtonText; + } + + .unified-extensions-item-action-button[disabled].subviewbutton { + background-color: Canvas; + color: GrayText !important; /* override panelUI-shared.css */ + opacity: 1 !important; /* override panelUI-shared.css */ + } + + .unified-extensions-item[attention] > .unified-extensions-item-action-button.subviewbutton:hover:not(:active) { + --uei-button-attention-dot-color: ButtonFace; + } + + .unified-extensions-item-message { + color: inherit; + } + + .unified-extensions-item > .unified-extensions-item-action-button.subviewbutton:hover:not([disabled]), + .unified-extensions-item > .unified-extensions-item-menu-button.subviewbutton:hover > .toolbarbutton-icon { + background-color: var(--uei-button-hover-bgcolor); + color: var(--uei-button-hover-color); + border-color: var(--uei-button-hover-bgcolor); + } + + .unified-extensions-item > .unified-extensions-item-action-button.subviewbutton:hover:active:not([disabled]), + .unified-extensions-item > .unified-extensions-item-menu-button.subviewbutton:hover:active > .toolbarbutton-icon { + background-color: var(--uei-button-active-bgcolor); + color: var(--uei-button-active-color); + border-color: var(--uei-button-active-color); + } + + .unified-extensions-item > .unified-extensions-item-menu-button.subviewbutton:focus-visible > .toolbarbutton-icon { + /* The border would otherwise overlap with the focus outline, causing an + * unsightly anti-aliasing artifact */ + border-color: transparent; + } } } diff --git a/browser/themes/shared/autocomplete.css b/browser/themes/shared/autocomplete.css index 5230700e38..62c82897b7 100644 --- a/browser/themes/shared/autocomplete.css +++ b/browser/themes/shared/autocomplete.css @@ -126,6 +126,10 @@ padding-inline-start: 0; } + &[originaltype="action"] > .two-line-wrapper { + flex: 1; + } + &[originaltype="generatedPassword"] { &:not([collapsed="true"]) { /* Workaround bug 451997 and/or bug 492645 */ @@ -156,6 +160,36 @@ border-top: 1px solid hsla(210,4%,10%,.14); } + &[originaltype="action"] { + text-align: center; + } + + /* status items */ + > .ac-status { + padding: var(--space-xsmall) var(--space-small); + text-align: center; + background-color: var(--color-background-information); + width: 100%; + border-bottom: 1px solid rgba(38,38,38,.15); + font-size: calc(10 / 12 * 1em); + } + + &:has(> .ac-status) { + opacity: 1; + } + + &[originaltype="autofill"] > .two-line-wrapper { + display: grid; + grid-template-columns: 32px 1fr; + + > .ac-site-icon { + width: auto; + height: 16px; + max-width: 32px; + max-height: 16px; + } + } + /* Insecure field warning */ &[originaltype="insecureWarning"] { background-color: var(--arrowpanel-dimmed); diff --git a/browser/themes/shared/browser-custom-colors.css b/browser/themes/shared/browser-custom-colors.css index d98d56690f..2318731274 100644 --- a/browser/themes/shared/browser-custom-colors.css +++ b/browser/themes/shared/browser-custom-colors.css @@ -5,7 +5,7 @@ @namespace html url("http://www.w3.org/1999/xhtml"); @media not (prefers-contrast) { - :root:not(:-moz-lwtheme) { + :root:not([lwtheme]) { --button-primary-bgcolor: light-dark(rgb(0, 97, 224), rgb(0, 221, 255)); --button-primary-hover-bgcolor: light-dark(rgb(2, 80, 187), rgb(128, 235, 255)); --button-primary-active-bgcolor: light-dark(rgb(5, 62, 148), rgb(170, 242, 255)); @@ -47,8 +47,7 @@ --tab-selected-textcolor: light-dark(rgb(21, 20, 26), rgb(251, 251, 254)); --tab-icon-overlay-stroke: light-dark(rgb(255, 255, 255), rgb(66, 65, 77)); --tab-icon-overlay-fill: light-dark(rgb(91, 91, 102), rgb(251, 251, 254)); - --tab-attention-icon-color: light-dark(rgb(42, 195, 162), rgb(84, 255, 189)); - --tabs-navbar-shadow-color: transparent; + --tabs-navbar-separator-style: none; --toolbox-non-lwt-bgcolor: light-dark(rgb(240, 240, 244), rgb(28, 27, 34)); --toolbox-non-lwt-textcolor: light-dark(rgb(21, 20, 26), rgb(251, 251, 254)); @@ -88,5 +87,6 @@ --chrome-content-separator-color: light-dark(rgb(204, 204, 204), hsl(240, 5%, 5%)); --link-color: light-dark(rgb(0, 97, 224), rgb(0, 221, 255)); + --attention-dot-color: light-dark(#2ac3a2, #54ffbd); } } diff --git a/browser/themes/shared/browser-shared.css b/browser/themes/shared/browser-shared.css index f708347193..276799c35f 100644 --- a/browser/themes/shared/browser-shared.css +++ b/browser/themes/shared/browser-shared.css @@ -18,6 +18,7 @@ @import url("chrome://browser/skin/places/editBookmarkPanel.css"); @import url("chrome://browser/skin/sidebar.css"); @import url("chrome://browser/skin/tabs.css"); +@import url("chrome://browser/content/tabpreview/tabpreview.css"); @import url("chrome://browser/skin/fullscreen/warning.css"); @import url("chrome://browser/skin/ctrlTab.css"); @import url("chrome://browser/skin/customizableui/customizeMode.css"); @@ -39,9 +40,8 @@ --toolbarbutton-border-radius: 4px; --chrome-content-separator-color: ThreeDShadow; - - --tabs-navbar-shadow-size: 1px; - --tabs-navbar-shadow-color: ThreeDShadow; + --tabs-navbar-separator-color: ThreeDShadow; + --tabs-navbar-separator-style: solid; --panelui-subview-transition-duration: 150ms; @@ -104,12 +104,13 @@ --toolbox-non-lwt-textcolor-inactive: InactiveCaptionText; } - &:-moz-lwtheme { + &[lwtheme] { color: var(--lwt-text-color); --link-color: light-dark(rgb(0, 97, 224), rgb(0, 221, 255)); --chrome-content-separator-color: rgba(0,0,0,.3); - --tabs-navbar-shadow-color: light-dark(rgba(0,0,0,.1), rgba(0,0,0,.3)); + --tabs-navbar-separator-color: light-dark(rgba(0,0,0,.1), rgba(0,0,0,.3)); + --attention-dot-color: light-dark(#2ac3a2, #54ffbd); @media not (prefers-contrast) { --focus-outline-color: light-dark(#0061E0, #00DDFF); @@ -139,21 +140,13 @@ @media (prefers-reduced-motion) { --inactive-window-transition: 0s; } - - @media (min-resolution: 1.5dppx) { - --tabs-navbar-shadow-size: 0.5px; - } - - @media (min-resolution: 3dppx) { - --tabs-navbar-shadow-size: 0.33px; - } } #navigator-toolbox { appearance: none; /* Toolbar / content area border */ - border-bottom: 1px solid var(--chrome-content-separator-color); + border-bottom: 0.01px solid var(--chrome-content-separator-color); background-color: var(--toolbox-non-lwt-bgcolor); color: var(--toolbox-non-lwt-textcolor); @@ -170,7 +163,7 @@ border-bottom-style: none; } - &:-moz-lwtheme { + :root[lwtheme] & { background-image: var(--lwt-additional-images); background-repeat: var(--lwt-background-tiling); background-position: var(--lwt-background-alignment); @@ -218,15 +211,19 @@ padding-inline-start: var(--toolbar-start-end-padding); } -:root[sessionrestored] #nav-bar:-moz-lwtheme { - transition: var(--ext-theme-background-transition); -} - -#nav-bar:not([tabs-hidden="true"]) { - /* The toolbar buttons that animate are only visible when the #TabsToolbar is not collapsed. - The animations use position:absolute and require a positioned #nav-bar. */ +#nav-bar { + /* The toolbar buttons that animate use position:absolute and require a + * positioned #nav-bar. */ position: relative; - box-shadow: 0 calc(-1 * var(--tabs-navbar-shadow-size)) 0 var(--tabs-navbar-shadow-color); + border-top: 0.01px var(--tabs-navbar-separator-style) var(--tabs-navbar-separator-color); + + &[tabs-hidden] { + border-top-style: none; + } + + :root[sessionrestored][lwtheme] & { + transition: var(--ext-theme-background-transition); + } } /* Bookmarks toolbar */ @@ -480,22 +477,22 @@ menupopup::part(drop-indicator) { color-scheme: var(--toolbar-color-scheme); border-top-color: var(--chrome-content-separator-color, ThreeDShadow); - &:-moz-lwtheme { + :root[lwtheme] & { background-color: var(--lwt-accent-color); background-image: linear-gradient(var(--toolbar-bgcolor), var(--toolbar-bgcolor)), var(--lwt-additional-images); background-repeat: no-repeat, var(--lwt-background-tiling); background-position: right bottom, var(--lwt-background-alignment); background-position-y: bottom !important; + } - :root:not([lwtheme-image]) &:-moz-window-inactive { - background-color: var(--lwt-accent-color-inactive, var(--lwt-accent-color)); - } + :root[lwtheme]:not([lwtheme-image]) &:-moz-window-inactive { + background-color: var(--lwt-accent-color-inactive, var(--lwt-accent-color)); + } - :root[lwtheme-image] & { - background-image: linear-gradient(var(--toolbar-bgcolor), var(--toolbar-bgcolor)), var(--lwt-header-image), var(--lwt-additional-images); - background-repeat: no-repeat, no-repeat, var(--lwt-background-tiling); - background-position: center, right bottom, var(--lwt-background-alignment); - } + :root[lwtheme-image] & { + background-image: linear-gradient(var(--toolbar-bgcolor), var(--toolbar-bgcolor)), var(--lwt-header-image), var(--lwt-additional-images); + background-repeat: no-repeat, no-repeat, var(--lwt-background-tiling); + background-position: center, right bottom, var(--lwt-background-alignment); } } diff --git a/browser/themes/shared/customizableui/customizeMode.css b/browser/themes/shared/customizableui/customizeMode.css index b3ee1ada6c..a1bb6a62f9 100644 --- a/browser/themes/shared/customizableui/customizeMode.css +++ b/browser/themes/shared/customizableui/customizeMode.css @@ -24,18 +24,18 @@ background-color: var(--toolbar-non-lwt-bgcolor); color: var(--toolbar-color); color-scheme: var(--toolbar-color-scheme); -} -#customization-container:-moz-lwtheme { - /* Ensure this displays on top of the non-lwt bgcolor, to make sure it's not - * semi-transparent */ - background-image: linear-gradient(var(--toolbar-bgcolor), var(--toolbar-bgcolor)); -} + :root[lwtheme] & { + /* Ensure this displays on top of the non-lwt bgcolor, to make sure it's not + * semi-transparent */ + background-image: linear-gradient(var(--toolbar-bgcolor), var(--toolbar-bgcolor)); + } -:root[lwtheme-image] #customization-container { - background-image: none; - color: var(--toolbar-non-lwt-textcolor); - text-shadow: none; + :root[lwtheme-image] & { + background-image: none; + color: var(--toolbar-non-lwt-textcolor); + text-shadow: none; + } } :root[lwtheme-image] #customization-palette .toolbarbutton-1 { diff --git a/browser/themes/shared/customizableui/panelUI-shared.css b/browser/themes/shared/customizableui/panelUI-shared.css index 51ab66b25a..07994d3233 100644 --- a/browser/themes/shared/customizableui/panelUI-shared.css +++ b/browser/themes/shared/customizableui/panelUI-shared.css @@ -33,7 +33,7 @@ --panel-and-palette-icon-size: 16px; - &:not(:-moz-lwtheme) { + &:not([lwtheme]) { --panel-separator-zap-gradient: linear-gradient(90deg, #9059FF 0%, #FF4AA2 52.08%, #FFBD4F 100%); } @@ -1655,13 +1655,13 @@ radiogroup:focus-visible > .subviewradio[focused="true"] { } #protections-popup { - #messaging-system-message-container { + #info-message-container { height: 260px; overflow: hidden; transition: margin-bottom .25s; } - #messaging-system-message-container[disabled] { + #info-message-container[disabled] { /* Offset the height when hidden. This makes the panel content * cover the info message and reveal it as it slides down, rather * than the info message growing in height. */ @@ -1669,7 +1669,7 @@ radiogroup:focus-visible > .subviewradio[focused="true"] { pointer-events: none; } - #messaging-system-message-container[disabled] #protections-popup-message { + #info-message-container[disabled] #protections-popup-message { opacity: 0; } } @@ -2122,7 +2122,7 @@ panelview { &.sent-view { @media not (prefers-contrast) { - background-color: var(--color-background-success); + background-color: var(--background-color-success); } > .panel-header { diff --git a/browser/themes/shared/downloads/progressmeter.css b/browser/themes/shared/downloads/progressmeter.css index 3184a63ed5..ad9f5f5f7d 100644 --- a/browser/themes/shared/downloads/progressmeter.css +++ b/browser/themes/shared/downloads/progressmeter.css @@ -8,12 +8,13 @@ --download-progress-fill-color: AccentColor; --download-progress-paused-color: GrayText; --download-progress-flare-color: rgba(255,255,255,0.75); -} - -/* download progress bar is used in contexts which are not LightweightThemeConsumers and - do not get the lwt- theme variables. So we duplicate the fallbacks here. */ -:root:-moz-lwtheme { - --download-progress-fill-color: var(--lwt-toolbarbutton-icon-fill-attention, light-dark(rgb(0, 97, 224), rgb(0, 221, 255))); + &[lwtheme] { + /* download progress bar is used in contexts which are not LightweightThemeConsumers and + do not get the lwt- theme variables. So we duplicate the fallbacks here. + FIXME(emilio): But then how do we get the lwtheme attribute? I think the + above makes no sense */ + --download-progress-fill-color: var(--lwt-toolbarbutton-icon-fill-attention, light-dark(rgb(0, 97, 224), rgb(0, 221, 255))); + } } @media (prefers-color-scheme: dark) { diff --git a/browser/themes/shared/formautofill-notification.css b/browser/themes/shared/formautofill-notification.css index 918f977d74..b168102f6b 100644 --- a/browser/themes/shared/formautofill-notification.css +++ b/browser/themes/shared/formautofill-notification.css @@ -75,7 +75,7 @@ } &#address-capture-edit-address-button { - background-image: url("chrome://browser/skin/formautofill/icon-address-edit.svg"); + background-image: url("chrome://global/skin/icons/edit-outline.svg"); } &#address-capture-menu-button { diff --git a/browser/themes/shared/formautofill/icon-address-edit.svg b/browser/themes/shared/formautofill/icon-address-edit.svg deleted file mode 100644 index aef1625941..0000000000 --- a/browser/themes/shared/formautofill/icon-address-edit.svg +++ /dev/null @@ -1,6 +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/. --> -<svg width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg"> -<path d="M0.0184994 13.6645L0.612501 10.4635C0.687501 10.0545 0.884501 9.6805 1.1805 9.3825L9.9815 0.5805C10.7555 -0.1925 12.0145 -0.1945 12.7915 0.5805L14.4195 2.2085C15.1935 2.9835 15.1935 4.2435 14.4195 5.0185L5.6155 13.8215C5.3195 14.1165 4.9455 14.3125 4.5375 14.3875L1.3355 14.9815C1.2655 14.9935 1.1975 15.0005 1.1295 15.0005C0.8325 15.0005 0.544499 14.8835 0.3305 14.6695C0.0674992 14.4055 -0.0495005 14.0305 0.0184994 13.6645ZM12.4715 5.1965L13.6315 4.0365L13.6305 3.1885L11.8105 1.3675L10.9625 1.3685L9.8025 2.5285L12.4715 5.1965ZM4.3105 13.1585C4.4705 13.1285 4.6175 13.0515 4.7335 12.9345L11.5865 6.0815L8.9185 3.4135L2.0655 10.2655C1.9485 10.3835 1.8715 10.5305 1.8405 10.6915L1.3665 13.2485L1.7515 13.6335L4.3105 13.1585Z"/> -</svg> diff --git a/browser/themes/shared/icons/circle-check-dotted.svg b/browser/themes/shared/icons/circle-check-dotted.svg index b498d1282e..28082dc98e 100644 --- a/browser/themes/shared/icons/circle-check-dotted.svg +++ b/browser/themes/shared/icons/circle-check-dotted.svg @@ -1,4 +1,7 @@ <!-- 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/. --> -<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g clip-path="url(#a)"><g fill="#000" clip-path="url(#b)"><path d="M7.18 10.944h.002l.397-.008a.372.372 0 0 0 .26-.114L12.3 6.203a.718.718 0 0 0-.02-1.021.742.742 0 0 0-1.022.02l-3.912 4.05-2.203-2.12a.72.72 0 0 0-.997 1.046l2.769 2.663a.372.372 0 0 0 .265.104ZM1.092 4.587l.634.392a.373.373 0 0 0 .522-.138c.12-.22.24-.415.368-.598a.37.37 0 0 0-.11-.527l-.63-.39a.373.373 0 0 0-.503.105c-.15.218-.288.44-.41.661a.37.37 0 0 0 .129.495ZM2.435 11.65a.37.37 0 0 0-.041-.291 6.436 6.436 0 0 1-.33-.62.372.372 0 0 0-.513-.169l-.657.352a.37.37 0 0 0-.16.486c.11.23.234.46.37.685a.372.372 0 0 0 .494.136l.654-.35a.371.371 0 0 0 .183-.23ZM3.131 3.028a.372.372 0 0 0 .538.05c.174-.154.36-.3.552-.435a.371.371 0 0 0 .078-.533l-.46-.583a.372.372 0 0 0-.508-.072 7.978 7.978 0 0 0-.611.482.37.37 0 0 0-.048.509l.46.582ZM1.517 9.535a.371.371 0 0 0 .061-.286 6.562 6.562 0 0 1-.101-.695.37.37 0 0 0-.423-.334l-.736.106a.37.37 0 0 0-.316.402c.024.256.061.515.11.77a.372.372 0 0 0 .419.296l.736-.106c.1-.015.19-.07.25-.153ZM5.116 1.67a.371.371 0 0 0 .488.229c.237-.092.456-.165.667-.221a.372.372 0 0 0 .257-.475L6.295.5a.372.372 0 0 0-.45-.242c-.249.068-.498.15-.742.243a.37.37 0 0 0-.22.462l.233.706ZM.355 7.143l.727.151a.376.376 0 0 0 .288-.058.37.37 0 0 0 .156-.25c.033-.221.082-.452.144-.686a.37.37 0 0 0-.284-.458L.658 5.69a.372.372 0 0 0-.436.27 8.027 8.027 0 0 0-.158.761.37.37 0 0 0 .291.42ZM3.373 12.588a.372.372 0 0 0-.54.016l-.494.553a.37.37 0 0 0 .016.511c.19.187.385.361.58.52a.37.37 0 0 0 .511-.042l.496-.554a.37.37 0 0 0-.045-.536 6.578 6.578 0 0 1-.524-.468ZM5.884 14.146a6.482 6.482 0 0 1-.652-.262.373.373 0 0 0-.501.2l-.277.69a.37.37 0 0 0 .191.475c.236.107.48.204.725.288a.372.372 0 0 0 .465-.213l.276-.689a.37.37 0 0 0-.227-.489Z"/><path d="M15.975 7.455a.381.381 0 0 0 0-.066 8.037 8.037 0 0 0-.067-.541l-.01-.08a2.289 2.289 0 0 0-.02-.148.35.35 0 0 0-.018-.067 7.917 7.917 0 0 0-.54-1.76.376.376 0 0 0-.029-.095 6.757 6.757 0 0 0-.219-.429l-.028-.053-.018-.035-.031-.061a1.91 1.91 0 0 0-.06-.114.342.342 0 0 0-.062-.078 8.033 8.033 0 0 0-1.12-1.47.383.383 0 0 0-.04-.05l-.044-.041a3.21 3.21 0 0 0-.118-.11l-.14-.13a5.444 5.444 0 0 0-.269-.248.362.362 0 0 0-.05-.036 8.05 8.05 0 0 0-1.56-1.005.378.378 0 0 0-.08-.052 2.155 2.155 0 0 0-.136-.058l-.073-.03a7.958 7.958 0 0 0-.315-.131l-.068-.03a2.014 2.014 0 0 0-.126-.051.347.347 0 0 0-.08-.02A7.974 7.974 0 0 0 7.98 0a.371.371 0 0 0-.372.37v.198l-.026.534a.37.37 0 0 0 .103.274c.07.074.177.123.27.115.231 0 .47.01.703.034a.338.338 0 0 0 .073 0 6.487 6.487 0 0 1 3.556 1.587c.117.102.233.205.344.317a6.533 6.533 0 0 1 1.883 4.573 6.534 6.534 0 0 1-.155 1.396 6.554 6.554 0 0 1-4.43 4.826l-.036.012a6.533 6.533 0 0 1-1.808.284c-.023-.005-.053-.005-.07-.006-.239.003-.47-.011-.702-.035a.37.37 0 0 0-.41.35l-.036.74a.371.371 0 0 0 .335.388c.231.023.465.036.696.038.027.006.054.01.082.01.626 0 1.27-.082 1.916-.241a.369.369 0 0 0 .082-.012 1.99 1.99 0 0 0 .123-.037l.064-.02c.123-.035.244-.072.364-.112l.066-.02c.043-.012.085-.024.127-.04a.35.35 0 0 0 .078-.039 8.003 8.003 0 0 0 1.67-.857.391.391 0 0 0 .042-.025c.079-.054.155-.114.231-.173l.178-.136a4.094 4.094 0 0 0 .249-.198c.464-.395.888-.85 1.261-1.356a.374.374 0 0 0 .075-.077c.028-.04.053-.08.079-.121l.073-.113c.094-.138.188-.276.27-.42a.34.34 0 0 0 .037-.093 7.947 7.947 0 0 0 .714-1.698.37.37 0 0 0 .024-.063 1.98 1.98 0 0 0 .03-.13l.015-.069a8.59 8.59 0 0 0 .127-.559.34.34 0 0 0 .005-.062 7.86 7.86 0 0 0 .12-1.332c0-.184-.013-.366-.026-.547Z"/></g></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath><clipPath id="b"><path fill="#fff" d="M0 0h16v16.004H0z"/></clipPath></defs></svg> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity"> + <path d="M7.18 10.944h.002l.397-.008a.372.372 0 0 0 .26-.114L12.3 6.203a.718.718 0 0 0-.02-1.021.742.742 0 0 0-1.022.02l-3.912 4.05-2.203-2.12a.72.72 0 0 0-.997 1.046l2.769 2.663a.372.372 0 0 0 .265.104ZM1.092 4.587l.634.392a.373.373 0 0 0 .522-.138c.12-.22.24-.415.368-.598a.37.37 0 0 0-.11-.527l-.63-.39a.373.373 0 0 0-.503.105c-.15.218-.288.44-.41.661a.37.37 0 0 0 .129.495ZM2.435 11.65a.37.37 0 0 0-.041-.291 6.436 6.436 0 0 1-.33-.62.372.372 0 0 0-.513-.169l-.657.352a.37.37 0 0 0-.16.486c.11.23.234.46.37.685a.372.372 0 0 0 .494.136l.654-.35a.371.371 0 0 0 .183-.23ZM3.131 3.028a.372.372 0 0 0 .538.05c.174-.154.36-.3.552-.435a.371.371 0 0 0 .078-.533l-.46-.583a.372.372 0 0 0-.508-.072 7.978 7.978 0 0 0-.611.482.37.37 0 0 0-.048.509l.46.582ZM1.517 9.535a.371.371 0 0 0 .061-.286 6.562 6.562 0 0 1-.101-.695.37.37 0 0 0-.423-.334l-.736.106a.37.37 0 0 0-.316.402c.024.256.061.515.11.77a.372.372 0 0 0 .419.296l.736-.106c.1-.015.19-.07.25-.153ZM5.116 1.67a.371.371 0 0 0 .488.229c.237-.092.456-.165.667-.221a.372.372 0 0 0 .257-.475L6.295.5a.372.372 0 0 0-.45-.242c-.249.068-.498.15-.742.243a.37.37 0 0 0-.22.462l.233.706ZM.355 7.143l.727.151a.376.376 0 0 0 .288-.058.37.37 0 0 0 .156-.25c.033-.221.082-.452.144-.686a.37.37 0 0 0-.284-.458L.658 5.69a.372.372 0 0 0-.436.27 8.027 8.027 0 0 0-.158.761.37.37 0 0 0 .291.42ZM3.373 12.588a.372.372 0 0 0-.54.016l-.494.553a.37.37 0 0 0 .016.511c.19.187.385.361.58.52a.37.37 0 0 0 .511-.042l.496-.554a.37.37 0 0 0-.045-.536 6.578 6.578 0 0 1-.524-.468ZM5.884 14.146a6.482 6.482 0 0 1-.652-.262.373.373 0 0 0-.501.2l-.277.69a.37.37 0 0 0 .191.475c.236.107.48.204.725.288a.372.372 0 0 0 .465-.213l.276-.689a.37.37 0 0 0-.227-.489Z"/> + <path d="M15.975 7.455a.381.381 0 0 0 0-.066 8.037 8.037 0 0 0-.067-.541l-.01-.08a2.289 2.289 0 0 0-.02-.148.35.35 0 0 0-.018-.067 7.917 7.917 0 0 0-.54-1.76.376.376 0 0 0-.029-.095 6.757 6.757 0 0 0-.219-.429l-.028-.053-.018-.035-.031-.061a1.91 1.91 0 0 0-.06-.114.342.342 0 0 0-.062-.078 8.033 8.033 0 0 0-1.12-1.47.383.383 0 0 0-.04-.05l-.044-.041a3.21 3.21 0 0 0-.118-.11l-.14-.13a5.444 5.444 0 0 0-.269-.248.362.362 0 0 0-.05-.036 8.05 8.05 0 0 0-1.56-1.005.378.378 0 0 0-.08-.052 2.155 2.155 0 0 0-.136-.058l-.073-.03a7.958 7.958 0 0 0-.315-.131l-.068-.03a2.014 2.014 0 0 0-.126-.051.347.347 0 0 0-.08-.02A7.974 7.974 0 0 0 7.98 0a.371.371 0 0 0-.372.37v.198l-.026.534a.37.37 0 0 0 .103.274c.07.074.177.123.27.115.231 0 .47.01.703.034a.338.338 0 0 0 .073 0 6.487 6.487 0 0 1 3.556 1.587c.117.102.233.205.344.317a6.533 6.533 0 0 1 1.883 4.573 6.534 6.534 0 0 1-.155 1.396 6.554 6.554 0 0 1-4.43 4.826l-.036.012a6.533 6.533 0 0 1-1.808.284c-.023-.005-.053-.005-.07-.006-.239.003-.47-.011-.702-.035a.37.37 0 0 0-.41.35l-.036.74a.371.371 0 0 0 .335.388c.231.023.465.036.696.038.027.006.054.01.082.01.626 0 1.27-.082 1.916-.241a.369.369 0 0 0 .082-.012 1.99 1.99 0 0 0 .123-.037l.064-.02c.123-.035.244-.072.364-.112l.066-.02c.043-.012.085-.024.127-.04a.35.35 0 0 0 .078-.039 8.003 8.003 0 0 0 1.67-.857.391.391 0 0 0 .042-.025c.079-.054.155-.114.231-.173l.178-.136a4.094 4.094 0 0 0 .249-.198c.464-.395.888-.85 1.261-1.356a.374.374 0 0 0 .075-.077c.028-.04.053-.08.079-.121l.073-.113c.094-.138.188-.276.27-.42a.34.34 0 0 0 .037-.093 7.947 7.947 0 0 0 .714-1.698.37.37 0 0 0 .024-.063 1.98 1.98 0 0 0 .03-.13l.015-.069a8.59 8.59 0 0 0 .127-.559.34.34 0 0 0 .005-.062 7.86 7.86 0 0 0 .12-1.332c0-.184-.013-.366-.026-.547Z"/> +</svg> diff --git a/browser/themes/shared/icons/insights.svg b/browser/themes/shared/icons/insights.svg new file mode 100644 index 0000000000..1fadce366c --- /dev/null +++ b/browser/themes/shared/icons/insights.svg @@ -0,0 +1,6 @@ +<!-- 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 https://mozilla.org/MPL/2.0/. --> +<svg width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg"> +<path d="M2.62599 1.8015L2.44399 2.6505C2.42875 2.72246 2.39295 2.78844 2.34094 2.84045C2.28893 2.89246 2.22294 2.92826 2.15099 2.9435L1.30199 3.1255C0.899988 3.2125 0.899988 3.7865 1.30199 3.8725L2.15099 4.0555C2.22294 4.07074 2.28893 4.10654 2.34094 4.15855C2.39295 4.21056 2.42875 4.27654 2.44399 4.3485L2.62599 5.1975C2.71299 5.5995 3.28699 5.5995 3.37299 5.1975L3.55599 4.3485C3.57123 4.27654 3.60703 4.21056 3.65904 4.15855C3.71105 4.10654 3.77703 4.07074 3.84899 4.0555L4.69699 3.8725C5.09899 3.7855 5.09899 3.2115 4.69699 3.1255L3.84799 2.9435C3.77609 2.92814 3.71016 2.8923 3.65817 2.84031C3.60618 2.78832 3.57035 2.7224 3.55499 2.6505L3.37299 1.8015C3.28599 1.3995 2.71199 1.3995 2.62599 1.8015ZM9.99998 14.4995C9.62057 14.5056 9.25107 14.3782 8.95593 14.1397C8.66078 13.9013 8.45872 13.5667 8.38498 13.1945L8.02798 11.5275C7.97201 11.2687 7.84263 11.0316 7.65534 10.8445C7.46805 10.6574 7.23079 10.5282 6.97198 10.4725L5.30498 10.1155C4.52398 9.9465 3.99998 9.2975 3.99998 8.4995C3.99998 7.7015 4.52398 7.0525 5.30498 6.8845L6.97198 6.5275C7.23077 6.47135 7.46793 6.34193 7.65517 6.15468C7.84242 5.96744 7.97184 5.73028 8.02798 5.4715L8.38598 3.8045C8.55298 3.0235 9.20198 2.4995 9.99998 2.4995C10.798 2.4995 11.447 3.0235 11.615 3.8045L11.972 5.4715C12.086 5.9995 12.5 6.4135 13.028 6.5265L14.695 6.8835C15.476 7.0525 16 7.7015 16 8.4995C16 9.2975 15.476 9.9465 14.695 10.1145L13.028 10.4715C12.769 10.5275 12.5316 10.657 12.3443 10.8445C12.157 11.0319 12.0277 11.2694 11.972 11.5285L11.614 13.1945C11.5406 13.5667 11.3388 13.9014 11.0438 14.1399C10.7488 14.3784 10.3793 14.5057 9.99998 14.4995ZM9.99998 3.7495C9.90199 3.7495 9.66898 3.7795 9.60798 4.0655L9.24998 5.7335C9.14331 6.22777 8.89642 6.68082 8.53886 7.03837C8.18131 7.39593 7.72826 7.64283 7.23398 7.7495L5.56698 8.1065C5.28098 8.1685 5.24998 8.4015 5.24998 8.4995C5.24998 8.5975 5.28098 8.8305 5.56698 8.8925L7.23398 9.2495C7.72826 9.35617 8.18131 9.60307 8.53886 9.96062C8.89642 10.3182 9.14331 10.7712 9.24998 11.2655L9.60798 12.9335C9.66898 13.2195 9.90199 13.2495 9.99998 13.2495C10.098 13.2495 10.331 13.2195 10.392 12.9335L10.75 11.2665C10.8565 10.772 11.1033 10.3188 11.4609 9.96103C11.8184 9.60329 12.2716 9.35624 12.766 9.2495L14.433 8.8925C14.719 8.8305 14.75 8.5975 14.75 8.4995C14.75 8.4015 14.719 8.1685 14.433 8.1065L12.766 7.7495C12.2717 7.64283 11.8187 7.39593 11.4611 7.03837C11.1036 6.68082 10.8567 6.22777 10.75 5.7335L10.392 4.0655C10.331 3.7795 10.098 3.7495 9.99998 3.7495ZM2.44399 12.6505L2.62599 11.8015C2.71199 11.3995 3.28599 11.3995 3.37299 11.8015L3.55499 12.6505C3.57035 12.7224 3.60618 12.7883 3.65817 12.8403C3.71016 12.8923 3.77609 12.9281 3.84799 12.9435L4.69699 13.1255C5.09899 13.2115 5.09899 13.7855 4.69699 13.8725L3.84799 14.0545C3.77609 14.0699 3.71016 14.1057 3.65817 14.1577C3.60618 14.2097 3.57035 14.2756 3.55499 14.3475L3.37299 15.1965C3.28699 15.5985 2.71299 15.5985 2.62599 15.1965L2.44399 14.3475C2.42863 14.2756 2.39279 14.2097 2.3408 14.1577C2.28881 14.1057 2.22289 14.0699 2.15099 14.0545L1.30199 13.8725C0.899988 13.7865 0.899988 13.2125 1.30199 13.1255L2.15099 12.9435C2.22294 12.9283 2.28893 12.8925 2.34094 12.8405C2.39295 12.7884 2.42875 12.7225 2.44399 12.6505Z"/> +</svg> diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn index 14a5efecac..e55e677fb3 100644 --- a/browser/themes/shared/jar.inc.mn +++ b/browser/themes/shared/jar.inc.mn @@ -70,7 +70,6 @@ skin/classic/browser/downloads/notification-start-animation.svg (../shared/downloads/notification-start-animation.svg) skin/classic/browser/downloads/progressmeter.css (../shared/downloads/progressmeter.css) skin/classic/browser/drm-icon.svg (../shared/drm-icon.svg) - skin/classic/browser/formautofill/icon-address-edit.svg (../shared/formautofill/icon-address-edit.svg) skin/classic/browser/formautofill/icon-capture-address-fields.svg (../shared/formautofill/icon-capture-address-fields.svg) skin/classic/browser/formautofill/icon-capture-email-fields.svg (../shared/formautofill/icon-capture-email-fields.svg) skin/classic/browser/formautofill/icon-doorhanger-menu.svg (../shared/formautofill/icon-doorhanger-menu.svg) @@ -179,6 +178,7 @@ skin/classic/browser/history.svg (../shared/icons/history.svg) skin/classic/browser/home.svg (../shared/icons/home.svg) skin/classic/browser/import.svg (../shared/icons/import.svg) + skin/classic/browser/insights.svg (../shared/icons/insights.svg) #ifndef MOZ_WIDGET_GTK skin/classic/browser/import-export.svg (../shared/icons/import-export.svg) #endif diff --git a/browser/themes/shared/pageInfo.css b/browser/themes/shared/pageInfo.css index 7d777c3cc4..7913144e51 100644 --- a/browser/themes/shared/pageInfo.css +++ b/browser/themes/shared/pageInfo.css @@ -12,11 +12,22 @@ } #topBar { - appearance: auto; - -moz-default-appearance: toolbar; + position: relative; -moz-window-dragging: drag; + padding-top: env(-moz-mac-titlebar-height); + border-bottom: 1px solid ThreeDShadow; + align-items: center; justify-content: center; + + &::after { + content: ""; + position: absolute; + inset: 0; + appearance: auto; + -moz-default-appearance: -moz-window-titlebar; + z-index: -1; + } } } diff --git a/browser/themes/shared/preferences/preferences.css b/browser/themes/shared/preferences/preferences.css index 8f2652f030..3d806d1394 100644 --- a/browser/themes/shared/preferences/preferences.css +++ b/browser/themes/shared/preferences/preferences.css @@ -518,7 +518,7 @@ a[is="moz-support-link"]:not(.sidebar-footer-link) { stroke: var(--in-content-item-selected); } -@media (prefers-contrast) { +@media (forced-colors) { #engineList > treechildren::-moz-tree-image(hover), #blocklistsTree > treechildren::-moz-tree-image(hover) { fill: var(--in-content-item-hover-text); @@ -624,7 +624,7 @@ html|dialog, } @media (prefers-color-scheme: dark) { - @media not (prefers-contrast) { + @media not (forced-colors) { html|dialog, .dialogBox { --in-content-page-background: #42414d; @@ -1323,7 +1323,7 @@ richlistitem .text-link:hover { border-radius: 4px; } -@media (prefers-contrast) { +@media (forced-colors) { .qr-code-box:not([hidden="true"]) { border: 1px solid currentColor; } diff --git a/browser/themes/shared/preferences/privacy.css b/browser/themes/shared/preferences/privacy.css index ca56927b02..df31828aa1 100644 --- a/browser/themes/shared/preferences/privacy.css +++ b/browser/themes/shared/preferences/privacy.css @@ -187,13 +187,13 @@ border-color: var(--in-content-accent-color); } -@media (prefers-contrast) { +@media (forced-colors) { .privacy-detailedoption.selected { outline: 2px solid var(--in-content-accent-color); } } -@media not (prefers-contrast) { +@media not (forced-colors) { .privacy-detailedoption { background-color: rgba(215, 215, 219, 0.1); } @@ -259,6 +259,10 @@ display: list-item; } +#dohExceptionsButton { + align-self: end; +} + .content-blocking-warning-image { list-style-image: url("chrome://global/skin/icons/warning.svg"); width: 16px; diff --git a/browser/themes/shared/preferences/translations.css b/browser/themes/shared/preferences/translations.css index 452d7d6c37..ddc55993a8 100644 --- a/browser/themes/shared/preferences/translations.css +++ b/browser/themes/shared/preferences/translations.css @@ -3,39 +3,61 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #translations-settings-header { - margin-top: 32px; - margin-bottom: 16px; + margin-top: var(--space-xlarge); + margin-bottom: calc( 2 * var(--space-small)); } -.translations-settings-manage-list { - overflow: auto; - background-color: var(--in-content-box-background); - border: 1px solid var(--in-content-box-border-color); - border-radius: var(--border-radius-small); - margin-top: 32px; +.translations-settings-manage-section { + margin-top: var(--space-xlarge); } .translations-settings-manage-language { - padding: 8px; + margin: 0 calc( 2 * var(--space-small)); display: flex; flex-direction: row; justify-content: space-between; - > h2 { - margin: 8px; - } } -.translations-settings-manage-list-info { +.translations-settings-manage-section-info { display: flex; flex-direction: column; - > h2 { - margin: 16px; - margin-bottom: 8px; + h2, p, a { + display: block; + margin: var(--space-small) calc( 2 * var(--space-small)); } - > p { - margin: 8px 16px; + a { + display: block; } - > a { - margin: 0 16px; +} + +.translations-settings-languages-card { + display: flex; + flex-direction: column; + max-height: calc( 7 * var(--space-xlarge)); + overflow: auto; + padding-inline: calc( 2 * var(--space-small)); +} + +.translations-settings-language-header { + margin: calc( 2 * var(--space-small)) 0; + font-size: var(--font-size-root); + font-weight: var(--font-weight-bold); +} + +.translations-settings-language { + display: flex; + align-items: center; + padding: var(--space-small) 0; + border-top: 1px solid var(--in-content-border-color); + label { + margin: 0px calc( 2 * var(--space-small)); } } + +.translations-settings-download-icon[type~="icon"]::part(button) { + background-image: url(chrome://browser/skin/downloads/downloads.svg); +} + +.translations-settings-delete-icon[type~="icon"]::part(button) { + background-image: url(chrome://global/skin/icons/delete.svg); +} diff --git a/browser/themes/shared/tabs.css b/browser/themes/shared/tabs.css index eb92f71e59..204df115be 100644 --- a/browser/themes/shared/tabs.css +++ b/browser/themes/shared/tabs.css @@ -17,12 +17,6 @@ --tab-border-radius: 4px; --tab-shadow-max-size: 6px; --tab-block-margin: 4px; - - --tab-attention-icon-color: AccentColor; - &:-moz-lwtheme { - --tab-attention-icon-color: light-dark(rgb(42, 195, 162), rgb(84, 255, 189)); - } - --tab-selected-textcolor: var(--toolbar-color); --tab-selected-bgcolor: var(--toolbar-bgcolor); --tab-selected-color-scheme: var(--toolbar-color-scheme); @@ -474,8 +468,8 @@ } @media not (prefers-contrast) { - #TabsToolbar #firefox-view-button[open]:not(:focus-visible) > .toolbarbutton-icon:-moz-lwtheme, - .tab-background[selected]:not([multiselected]):-moz-lwtheme { + :root[lwtheme] #TabsToolbar #firefox-view-button[open]:not(:focus-visible) > .toolbarbutton-icon, + :root[lwtheme] .tab-background[selected]:not([multiselected]) { outline: 1px solid var(--lwt-tab-line-color, var(--lwt-tabs-border-color, currentColor)); outline-offset: -1px; } @@ -498,8 +492,8 @@ .tabbrowser-tab:is([image], [pinned]) > .tab-stack > .tab-content[attention]:not([selected]), .tabbrowser-tab > .tab-stack > .tab-content[pinned][titlechanged]:not([selected]), #firefox-view-button[attention] { - background-image: radial-gradient(circle, var(--tab-attention-icon-color), var(--tab-attention-icon-color) 2px, transparent 2px); - background-position: center bottom calc(6.5px + var(--tabs-navbar-shadow-size)); + background-image: radial-gradient(circle, var(--attention-dot-color), var(--attention-dot-color) 2px, transparent 2px); + background-position: center bottom 6.5px; background-size: 4px 4px; background-repeat: no-repeat; } diff --git a/browser/themes/shared/toolbarbutton-icons.css b/browser/themes/shared/toolbarbutton-icons.css index 3879689f12..5700814351 100644 --- a/browser/themes/shared/toolbarbutton-icons.css +++ b/browser/themes/shared/toolbarbutton-icons.css @@ -6,14 +6,13 @@ --toolbarbutton-icon-fill-attention: AccentColor; --toolbarbutton-icon-fill-attention-text: AccentColorText; --warning-icon-bgcolor: light-dark(#FFA436, #FFBD4F); -} - -:root:-moz-lwtheme { - --toolbarbutton-icon-fill-attention: var(--lwt-toolbarbutton-icon-fill-attention, light-dark(rgb(0, 97, 224), rgb(0, 221, 255))); - /* FIXME(emilio): This could have poor contrast on some themes, maybe use - * contrast-color() once that's available, or compute a new variable in - * LightWeightThemeConsumer */ - --toolbarbutton-icon-fill-attention-text: var(--toolbar-field-background-color); + &[lwtheme] { + --toolbarbutton-icon-fill-attention: var(--lwt-toolbarbutton-icon-fill-attention, light-dark(rgb(0, 97, 224), rgb(0, 221, 255))); + /* FIXME(emilio): This could have poor contrast on some themes, maybe use + * contrast-color() once that's available, or compute a new variable in + * LightWeightThemeConsumer */ + --toolbarbutton-icon-fill-attention-text: var(--toolbar-field-background-color); + } } .toolbarbutton-animatable-box, @@ -452,10 +451,6 @@ toolbarbutton.bookmark-item { list-style-image: url("chrome://global/skin/icons/folder.svg"); } -#whats-new-menu-button { - list-style-image: url("chrome://global/skin/icons/whatsnew.svg"); -} - #ion-button { list-style-image: url("chrome://browser/skin/ion.svg"); } diff --git a/browser/themes/shared/toolbarbuttons.css b/browser/themes/shared/toolbarbuttons.css index 7af8b2227d..2ba44e5132 100644 --- a/browser/themes/shared/toolbarbuttons.css +++ b/browser/themes/shared/toolbarbuttons.css @@ -113,7 +113,7 @@ toolbar .toolbarbutton-1 { } #TabsToolbar .toolbarbutton-1 { - margin: 0 0 var(--tabs-navbar-shadow-size); + margin: 0; > .toolbarbutton-icon, > .toolbarbutton-text, diff --git a/browser/themes/shared/translations/panel.css b/browser/themes/shared/translations/panel.css index 6777e37cc3..988dbadb4b 100644 --- a/browser/themes/shared/translations/panel.css +++ b/browser/themes/shared/translations/panel.css @@ -7,7 +7,7 @@ width: 31em; } -:where(#full-page-translations-panel) :is(description, label, menulist) { +:where(.translations-panel) :is(description, label, menulist) { margin: 0; } @@ -132,6 +132,7 @@ h1.translations-panel-header-wrapper { background-color: transparent; font: message-box; font-weight: var(--font-weight-bold); + &::before { content: url(chrome://global/skin/icons/edit-copy.svg); fill: currentColor; @@ -146,15 +147,33 @@ h1.translations-panel-header-wrapper { } .select-translations-panel-label { - margin-inline: 2px; + margin-block: var(--arrowpanel-padding) 6px; } #select-translations-panel-lang-selection { gap: 6px; } -#select-translations-panel-translation-area { +.select-translations-panel-text-area { height: 8em; - margin-inline: 5px; resize: none; + + &:disabled { + color: var(--panel-disabled-color); + border-color: var(--panel-disabled-color); + } + + &.translating { + -moz-context-properties: fill; + fill: currentColor; + background-image: url("chrome://global/skin/icons/loading.svg"); + background-repeat: no-repeat; + background-position: 8px 8px; + background-size: var(--size-item-small); + padding-inline-start: calc(var(--size-item-small) + (2 * var(--space-small))); + + &:-moz-locale-dir(rtl) { + background-position-x: right 8px; + } + } } diff --git a/browser/themes/shared/urlbar-searchbar.css b/browser/themes/shared/urlbar-searchbar.css index 9fc88fbde8..b5684c5f6f 100644 --- a/browser/themes/shared/urlbar-searchbar.css +++ b/browser/themes/shared/urlbar-searchbar.css @@ -126,15 +126,17 @@ border-color: color-mix(in srgb, currentColor 20%, transparent); } -#urlbar-input:-moz-lwtheme::selection, -.searchbar-textbox:-moz-lwtheme::selection { - background-color: var(--lwt-toolbar-field-highlight, Highlight); - color: var(--lwt-toolbar-field-highlight-text, HighlightText); -} +:root[lwtheme] { + #urlbar-input::selection, + .searchbar-textbox::selection { + background-color: var(--lwt-toolbar-field-highlight, Highlight); + color: var(--lwt-toolbar-field-highlight-text, HighlightText); + } -#urlbar-input:not(:focus):-moz-lwtheme::selection, -.searchbar-textbox:not(:focus-within):-moz-lwtheme::selection { - background-color: var(--lwt-toolbar-field-highlight, text-select-disabled-background); + #urlbar-input:not(:focus)::selection, + .searchbar-textbox:not(:focus-within)::selection { + background-color: var(--lwt-toolbar-field-highlight, text-select-disabled-background); + } } #urlbar:not([focused="true"]) { @@ -193,13 +195,11 @@ position: absolute; width: 100%; height: var(--urlbar-height); - top: calc((var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2); - left: 0; -} -#urlbar[breakout] > #urlbar-input-container { - width: 100%; - height: 100%; + > #urlbar-input-container { + width: 100%; + height: 100%; + } } #urlbar:not([open]) > .urlbarView, @@ -219,12 +219,12 @@ top: 0; left: calc(-1 * var(--urlbar-margin-inline)); width: calc(100% + 2 * var(--urlbar-margin-inline)); -} -#urlbar[breakout][breakout-extend] > #urlbar-input-container { - height: var(--urlbar-toolbar-height); - padding-block: calc((var(--urlbar-toolbar-height) - var(--urlbar-height)) / 2 + var(--urlbar-container-padding)); - padding-inline: calc(var(--urlbar-margin-inline) + var(--urlbar-container-padding)); + > #urlbar-input-container { + height: var(--urlbar-container-height); + padding-block: calc((var(--urlbar-container-height) - var(--urlbar-height)) / 2 + var(--urlbar-container-padding)); + padding-inline: calc(var(--urlbar-margin-inline) + var(--urlbar-container-padding)); + } } #urlbar.searchButton[breakout][breakout-extend] > #urlbar-input-container > #urlbar-search-button { @@ -376,7 +376,7 @@ /* As above, but for the default theme in dark mode, which suffers from the same issue */ @media (prefers-color-scheme: dark) { - &:not(:-moz-lwtheme) { + :root:not([lwtheme]) & { filter: grayscale(100%) brightness(20%) invert(); } } @@ -791,7 +791,7 @@ margin: var(--arrowpanel-menuitem-margin); width: auto; - & > #searchbar:-moz-lwtheme { + :root[lwtheme] & > #searchbar { /* Theme authors usually only consider contrast against the toolbar when picking a border color for the search bar. Since the search bar can be dragged into the overflow panel, we need to show a high-contrast border diff --git a/browser/themes/shared/urlbarView.css b/browser/themes/shared/urlbarView.css index ee8ee15c2a..c06342fd98 100644 --- a/browser/themes/shared/urlbarView.css +++ b/browser/themes/shared/urlbarView.css @@ -56,7 +56,7 @@ --urlbarView-action-slide-in-distance: -200px; } - &:-moz-lwtheme { + &[lwtheme] { --urlbarView-action-color: light-dark(rgb(91, 91, 102), rgb(191, 191, 201)); --urlbarView-highlight-background: light-dark(rgb(0, 99, 255), rgb(0, 99, 225)); --urlbarView-highlight-color: white; @@ -611,25 +611,14 @@ margin-inline-start: 0.35em; } -.urlbarView-userContext-iconMode { - display: none; -} - -.urlbarView-userContext-textMode { - display: inline-block; - > span { - font-variant: small-caps; - } +.urlbarView-userContext-textMode > span { + font-variant: small-caps; } /* Display userContext icon instead of text, when window is too narrow. */ -.urlbarView-results[wrap] { - .urlbarView-userContext-textMode { - display: none; - } - .urlbarView-userContext-iconMode { - display: inline-block; - } +.urlbarView-results[wrap] .urlbarView-userContext-textMode, +.urlbarView-results:not([wrap]) .urlbarView-userContext-iconMode { + display: none; } /* Tail suggestions */ @@ -722,17 +711,16 @@ color: var(--urlbar-box-text-color); background-color: var(--urlbar-box-focus-bgcolor); border-radius: var(--toolbarbutton-border-radius); - padding: 6px 8px; - - &.urlbarView-userContext { - padding-top: 2px; - } - margin-block: -6px; + padding: 4px 8px; + margin-block: -2px; margin-inline-start: 8px; :root[uidensity=compact] & { padding: 3px 6px; - margin-block: -3px; + } + + &.urlbarView-userContext { + padding-top: 0; } } diff --git a/browser/themes/triage.json b/browser/themes/triage.json index 30ef62e34f..bf761e201e 100644 --- a/browser/themes/triage.json +++ b/browser/themes/triage.json @@ -1,8 +1,5 @@ { "triagers": { - "Amy Churchwell": { - "bzmail": "achurchwell@mozilla.com" - }, "Cieara Meador": { "bzmail": "cmeador@mozilla.com" }, @@ -20,67 +17,66 @@ } }, "duty-start-dates": { - "2024-03-01": "Amy Churchwell", "2024-03-08": "Cieara Meador", "2024-03-15": "Dão Gottwald", "2024-03-22": "Jules Simplicio", "2024-03-29": "Kelly Cochrane", "2024-04-06": "Sam Foster", - "2024-04-13": "Amy Churchwell", - "2024-04-20": "Cieara Meador", - "2024-04-27": "Dão Gottwald", - "2024-05-04": "Jules Simplicio", - "2024-05-11": "Kelly Cochrane", - "2024-05-18": "Sam Foster", - "2024-05-25": "Amy Churchwell", - "2024-06-02": "Cieara Meador", - "2024-06-09": "Dão Gottwald", - "2024-06-16": "Jules Simplicio", - "2024-06-23": "Kelly Cochrane", - "2024-06-30": "Sam Foster", - "2024-07-07": "Amy Churchwell", - "2024-07-14": "Cieara Meador", - "2024-07-21": "Dão Gottwald", - "2024-07-28": "Jules Simplicio", - "2024-08-05": "Kelly Cochrane", - "2024-08-12": "Sam Foster", - "2024-08-19": "Amy Churchwell", - "2024-08-26": "Cieara Meador", - "2024-09-03": "Dão Gottwald", - "2024-09-10": "Jules Simplicio", - "2024-09-17": "Kelly Cochrane", - "2024-09-24": "Sam Foster", - "2024-10-01": "Amy Churchwell", + "2024-04-13": "Cieara Meador", + "2024-04-20": "Dão Gottwald", + "2024-04-27": "Jules Simplicio", + "2024-05-04": "Kelly Cochrane", + "2024-05-11": "Sam Foster", + "2024-05-18": "Cieara Meador", + "2024-05-25": "Dão Gottwald", + "2024-06-02": "Jules Simplicio", + "2024-06-09": "Kelly Cochrane", + "2024-06-16": "Sam Foster", + "2024-06-23": "Cieara Meador", + "2024-06-30": "Dão Gottwald", + "2024-07-07": "Jules Simplicio", + "2024-07-14": "Kelly Cochrane", + "2024-07-21": "Sam Foster", + "2024-07-28": "Cieara Meador", + "2024-08-05": "Dão Gottwald", + "2024-08-12": "Jules Simplicio", + "2024-08-19": "Kelly Cochrane", + "2024-08-26": "Sam Foster", + "2024-09-03": "Cieara Meador", + "2024-09-10": "Dão Gottwald", + "2024-09-17": "Jules Simplicio", + "2024-09-24": "Kelly Cochrane", + "2024-10-01": "Sam Foster", "2024-10-08": "Cieara Meador", "2024-10-15": "Dão Gottwald", "2024-10-22": "Jules Simplicio", "2024-10-29": "Kelly Cochrane", "2024-11-06": "Sam Foster", - "2024-11-13": "Amy Churchwell", - "2024-11-20": "Cieara Meador", - "2024-11-27": "Dão Gottwald", - "2024-12-04": "Jules Simplicio", - "2024-12-11": "Kelly Cochrane", - "2024-12-18": "Sam Foster", - "2024-12-25": "Amy Churchwell", - "2025-01-02": "Cieara Meador", - "2025-01-09": "Dão Gottwald", - "2025-01-16": "Jules Simplicio", - "2025-01-23": "Kelly Cochrane", - "2025-01-30": "Sam Foster", - "2025-02-07": "Amy Churchwell", - "2025-02-14": "Cieara Meador", - "2025-02-21": "Dão Gottwald", - "2025-02-28": "Jules Simplicio", - "2025-03-05": "Kelly Cochrane", - "2025-03-12": "Sam Foster", - "2025-03-19": "Amy Churchwell", - "2025-03-26": "Cieara Meador", - "2025-04-03": "Dão Gottwald", - "2025-04-10": "Jules Simplicio", - "2025-04-17": "Kelly Cochrane", - "2025-04-24": "Sam Foster", - "2025-05-01": "Amy Churchwell", + "2024-11-13": "Cieara Meador", + "2024-11-20": "Dão Gottwald", + "2024-11-27": "Jules Simplicio", + "2024-12-04": "Kelly Cochrane", + "2024-12-11": "Sam Foster", + "2024-12-18": "Cieara Meador", + "2024-12-25": "Dão Gottwald", + "2025-01-02": "Jules Simplicio", + "2025-01-09": "Kelly Cochrane", + "2025-01-16": "Sam Foster", + "2025-01-23": "Cieara Meador", + "2025-01-30": "Dão Gottwald", + "2025-02-07": "Jules Simplicio", + "2025-02-14": "Kelly Cochrane", + "2025-02-21": "Sam Foster", + "2025-02-28": "Cieara Meador", + "2025-03-05": "Dão Gottwald", + "2025-03-12": "Jules Simplicio", + "2025-03-19": "Kelly Cochrane", + "2025-03-26": "Sam Foster", + "2025-04-03": "Cieara Meador", + "2025-04-10": "Dão Gottwald", + "2025-04-17": "Jules Simplicio", + "2025-04-24": "Kelly Cochrane", + "2025-05-01": "Sam Foster", "2025-05-08": "Cieara Meador" } } diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index c4c2f814c7..982a7a9616 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -35,7 +35,7 @@ --toolbox-non-lwt-bgcolor-inactive: InactiveCaption; --toolbox-non-lwt-textcolor-inactive: InactiveCaptionText; - #TabsToolbar:not(:-moz-lwtheme) { + &:not([lwtheme]) #TabsToolbar { /* These colors match the Linux/HCM default button colors. We need to * override these on the tabs toolbar because the accent color is * arbitrary, so the hardcoded colors from browser-custom-colors might @@ -58,7 +58,7 @@ /* When temporarily showing the menu bar, make it at least as tall as the * tab bar such that the window controls don't appear to move up. */ :root[tabsintitlebar] #toolbar-menubar[autohide="true"] { - height: calc(var(--tab-min-height) - var(--tabs-navbar-shadow-size)); + height: var(--tab-min-height); } /* Titlebar */ @@ -76,12 +76,6 @@ .titlebar-buttonbox-container { align-items: stretch; - - /* Prevent window controls from overlapping the nav bar's shadow on the tab - * bar. */ - #TabsToolbar > & { - margin-bottom: var(--tabs-navbar-shadow-size); - } } /* Window control buttons */ @@ -309,10 +303,12 @@ --urlbar-box-hover-text-color: SelectedItemText; } - #urlbar:not(:-moz-lwtheme, [focused="true"]) > #urlbar-background, - #searchbar:not(:-moz-lwtheme, :focus-within), - .findbar-textbox:not(:-moz-lwtheme, :focus) { - border-color: ThreeDShadow; + :root:not([lwtheme]) { + #urlbar:not([focused="true"]) > #urlbar-background, + #searchbar:not(:focus-within), + .findbar-textbox:not(:focus) { + border-color: ThreeDShadow; + } } } diff --git a/browser/themes/windows/places/organizer.css b/browser/themes/windows/places/organizer.css index 68b0fd3866..dd171b7ecd 100644 --- a/browser/themes/windows/places/organizer.css +++ b/browser/themes/windows/places/organizer.css @@ -82,7 +82,6 @@ /* Toolbar and menus */ #placesToolbar { - appearance: none; background-color: var(--organizer-toolbar-background); color: var(--organizer-color); border-bottom: 1px solid var(--organizer-border-color); diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs index a2fe6195ab..d7e24ad9a5 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs @@ -95,7 +95,7 @@ export var Screenshot = { }, async _screenshotOSX(filename) { - let screencapture = (windowID = null) => { + let screencapture = () => { return new Promise((resolve, reject) => { // Get the screencapture executable let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); @@ -175,7 +175,7 @@ export var Screenshot = { _processObserver(resolve, reject) { return { - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "process-finished": try { diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs index 8309eab623..c044b557c6 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs @@ -5,7 +5,7 @@ import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; export var AppMenu = { - init(libDir) {}, + init() {}, configurations: { appMenuMainView: { diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs index 2bc12d9012..9a8bea5327 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs @@ -5,7 +5,7 @@ import { CustomizableUI } from "resource:///modules/CustomizableUI.sys.mjs"; export var Buttons = { - init(libDir) { + init() { createWidget(); }, diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs index 587b573ed3..391f52765b 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs @@ -26,7 +26,7 @@ const MIXED_PASSIVE_CONTENT_URL = `https://example.com/${RESOURCE_PATH}/mixed_pa const TRACKING_PAGE = `http://tracking.example.org/${RESOURCE_PATH}/tracking.html`; export var ControlCenter = { - init(libDir) {}, + init() {}, configurations: { about: { diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs index a5a1b65b69..e7b319fdd6 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs @@ -5,7 +5,7 @@ import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; export var CustomizeMode = { - init(libDir) {}, + init() {}, configurations: { notCustomizing: { diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs index addef011c0..d07b9be0e5 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs @@ -19,7 +19,7 @@ function selectToolbox(toolbox) { } export var DevTools = { - init(libDir) { + init() { let panels = [ "options", "webconsole", diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs index 91c3349ec7..c58a60c5c2 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs @@ -5,7 +5,7 @@ import { AddonManager } from "resource://gre/modules/AddonManager.sys.mjs"; export var LightweightThemes = { - init(libDir) {}, + init() {}, configurations: { noLWT: { diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs index de12c69e81..b49f1be949 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs @@ -12,7 +12,7 @@ const URL = let lastTab = null; export var PermissionPrompts = { - init(libDir) { + init() { Services.prefs.setBoolPref("media.navigator.permission.fake", true); Services.prefs.setBoolPref("extensions.install.requireBuiltInCerts", false); Services.prefs.setBoolPref("signon.rememberSignons", true); diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs index d988ba88cf..c205edee33 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs @@ -8,7 +8,7 @@ import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs"; export var Preferences = { - init(libDir) { + init() { let panes = [ ["paneGeneral"], ["paneGeneral", browsingGroup], diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs index 85b134b8bc..e9f82e15b5 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs @@ -10,7 +10,7 @@ import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs"; export var Tabs = { - init(libDir) {}, + init() {}, configurations: { fiveTabs: { @@ -20,7 +20,7 @@ export var Tabs = { let browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); hoverTab(browserWindow.gBrowser.tabs[3]); - await new Promise((resolve, reject) => { + await new Promise(resolve => { setTimeout(resolve, 3000); }); await allTabTitlesDisplayed(browserWindow); @@ -59,7 +59,7 @@ export var Tabs = { let newTabButton = browserWindow.gBrowser.tabContainer.newTabButton; hoverTab(newTabButton); - await new Promise((resolve, reject) => { + await new Promise(resolve => { setTimeout(resolve, 3000); }); await allTabTitlesDisplayed(browserWindow); @@ -117,7 +117,7 @@ export var Tabs = { browserWindow.gBrowser.selectTabAtIndex(3); hoverTab(browserWindow.gBrowser.tabs[5]); - await new Promise((resolve, reject) => { + await new Promise(resolve => { setTimeout(resolve, 3000); }); diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs index bc5b12c219..3c2973aa0a 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs @@ -5,7 +5,7 @@ const PREF_TABS_IN_TITLEBAR = "browser.tabs.inTitlebar"; export var TabsInTitlebar = { - init(libDir) {}, + init() {}, configurations: { tabsInTitlebar: { diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs index 06b1159a5e..86a0064437 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ export var Toolbars = { - init(libDir) {}, + init() {}, configurations: { onlyNavBar: { diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs index 37cc123727..9fa7cf32bf 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ export var UIDensities = { - init(libDir) {}, + init() {}, configurations: { compactDensity: { diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs index 98a4e3ec00..8a1694f731 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs @@ -6,7 +6,7 @@ import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { BrowserTestUtils } from "resource://testing-common/BrowserTestUtils.sys.mjs"; export var WindowSize = { - init(libDir) { + init() { Services.prefs.setBoolPref("browser.fullscreen.autohide", false); }, @@ -20,7 +20,7 @@ export var WindowSize = { // Wait for the Lion fullscreen transition to end as there doesn't seem to be an event // and trying to maximize while still leaving fullscreen doesn't work. - await new Promise((resolve, reject) => { + await new Promise(resolve => { setTimeout(function waitToLeaveFS() { browserWindow.maximize(); resolve(); @@ -36,7 +36,7 @@ export var WindowSize = { Services.wm.getMostRecentWindow("navigator:browser"); await toggleFullScreen(browserWindow, false); browserWindow.restore(); - await new Promise((resolve, reject) => { + await new Promise(resolve => { setTimeout(resolve, 5000); }); }, @@ -49,7 +49,7 @@ export var WindowSize = { Services.wm.getMostRecentWindow("navigator:browser"); await toggleFullScreen(browserWindow, true); // OS X Lion fullscreen transition takes a while - await new Promise((resolve, reject) => { + await new Promise(resolve => { setTimeout(resolve, 5000); }); }, |