summaryrefslogtreecommitdiffstats
path: root/browser
diff options
context:
space:
mode:
Diffstat (limited to 'browser')
-rw-r--r--browser/actors/AboutReaderParent.sys.mjs2
-rw-r--r--browser/actors/ClickHandlerParent.sys.mjs3
-rw-r--r--browser/actors/ContentSearchParent.sys.mjs6
-rw-r--r--browser/actors/ContextMenuChild.sys.mjs21
-rw-r--r--browser/actors/FormValidationChild.sys.mjs2
-rw-r--r--browser/actors/FormValidationParent.sys.mjs2
-rw-r--r--browser/actors/PluginParent.sys.mjs2
-rw-r--r--browser/actors/PromptParent.sys.mjs188
-rw-r--r--browser/actors/RefreshBlockerChild.sys.mjs6
-rw-r--r--browser/actors/ScreenshotsComponentChild.sys.mjs53
-rw-r--r--browser/actors/SearchSERPTelemetryChild.sys.mjs68
-rw-r--r--browser/actors/WebRTCChild.sys.mjs8
-rw-r--r--browser/actors/WebRTCParent.sys.mjs6
-rw-r--r--browser/app/Makefile.in14
-rw-r--r--browser/app/macbuild/Contents/Info.plist.in19
-rw-r--r--browser/app/moz.build2
-rw-r--r--browser/app/nmhproxy/Cargo.toml2
-rw-r--r--browser/app/nmhproxy/src/commands.rs162
-rw-r--r--browser/app/nmhproxy/src/main.rs9
-rw-r--r--browser/app/nsBrowserApp.cpp3
-rw-r--r--browser/app/profile/firefox.js178
-rw-r--r--browser/app/winlauncher/test/TestSameBinary.cpp3
-rw-r--r--browser/base/content/aboutDialog-appUpdater.js2
-rw-r--r--browser/base/content/aboutDialog.xhtml2
-rw-r--r--browser/base/content/appmenu-viewcache.inc.xhtml168
-rw-r--r--browser/base/content/browser-a11yUtils.js8
-rw-r--r--browser/base/content/browser-addons.js143
-rw-r--r--browser/base/content/browser-allTabsMenu.inc.xhtml4
-rw-r--r--browser/base/content/browser-allTabsMenu.js21
-rw-r--r--browser/base/content/browser-box.inc.xhtml3
-rw-r--r--browser/base/content/browser-captivePortal.js2
-rw-r--r--browser/base/content/browser-commands.js591
-rw-r--r--browser/base/content/browser-context.inc2
-rw-r--r--browser/base/content/browser-ctrlTab.js4
-rw-r--r--browser/base/content/browser-data-submission-info-bar.js2
-rw-r--r--browser/base/content/browser-fullScreenAndPointerLock.js43
-rw-r--r--browser/base/content/browser-gestureSupport.js10
-rw-r--r--browser/base/content/browser-init.js1107
-rw-r--r--browser/base/content/browser-menubar.inc16
-rw-r--r--browser/base/content/browser-pageActions.js4
-rw-r--r--browser/base/content/browser-places.js29
-rw-r--r--browser/base/content/browser-sets.inc46
-rw-r--r--browser/base/content/browser-siteIdentity.js15
-rw-r--r--browser/base/content/browser-sitePermissionPanel.js7
-rw-r--r--browser/base/content/browser-siteProtections.js39
-rw-r--r--browser/base/content/browser-sync.js336
-rw-r--r--browser/base/content/browser-tabsintitlebar.js2
-rw-r--r--browser/base/content/browser-thumbnails.js4
-rw-r--r--browser/base/content/browser-toolbarKeyNav.js2
-rw-r--r--browser/base/content/browser.css13
-rw-r--r--browser/base/content/browser.js2145
-rw-r--r--browser/base/content/browser.js.globals29
-rw-r--r--browser/base/content/browser.xhtml10
-rw-r--r--browser/base/content/contentTheme.js21
-rw-r--r--browser/base/content/macWindow.inc.xhtml1
-rw-r--r--browser/base/content/main-popupset.inc.xhtml57
-rw-r--r--browser/base/content/navigator-toolbox.inc.xhtml18
-rw-r--r--browser/base/content/nsContextMenu.js95
-rw-r--r--browser/base/content/pageinfo/pageInfo.js40
-rw-r--r--browser/base/content/pageinfo/pageInfo.xhtml4
-rw-r--r--browser/base/content/pageinfo/permissions.js2
-rw-r--r--browser/base/content/pageinfo/security.js2
-rw-r--r--browser/base/content/popup-notifications.inc2
-rw-r--r--browser/base/content/sanitizeDialog.js23
-rw-r--r--browser/base/content/spotlight.html1
-rw-r--r--browser/base/content/tabbrowser-tab.js26
-rw-r--r--browser/base/content/tabbrowser-tabs.js57
-rw-r--r--browser/base/content/tabbrowser.css101
-rw-r--r--browser/base/content/tabbrowser.js397
-rw-r--r--browser/base/content/test/about/browser.toml1
-rw-r--r--browser/base/content/test/about/browser_aboutCertError.js4
-rw-r--r--browser/base/content/test/about/browser_aboutNetError_csp_iframe.js4
-rw-r--r--browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js4
-rw-r--r--browser/base/content/test/alerts/browser.toml4
-rw-r--r--browser/base/content/test/alerts/browser_notification_close.js25
-rw-r--r--browser/base/content/test/alerts/browser_notification_open_settings.js2
-rw-r--r--browser/base/content/test/alerts/browser_notification_replace.js66
-rw-r--r--browser/base/content/test/alerts/head.js2
-rw-r--r--browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js2
-rw-r--r--browser/base/content/test/contextMenu/browser.toml6
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu.js71
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_badiframe.js2
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_cross_boundary_selection.js73
-rw-r--r--browser/base/content/test/contextMenu/browser_contextmenu_save_blocked.js2
-rw-r--r--browser/base/content/test/contextMenu/browser_strip_on_share_link.js12
-rw-r--r--browser/base/content/test/contextMenu/browser_strip_on_share_nested_link.js162
-rw-r--r--browser/base/content/test/contextMenu/contextmenu_common.js4
-rw-r--r--browser/base/content/test/contextMenu/subtst_contextmenu.html4
-rw-r--r--browser/base/content/test/contextMenu/subtst_contextmenu_webext.html2
-rw-r--r--browser/base/content/test/favicons/browser_favicon_change_not_in_document.js8
-rw-r--r--browser/base/content/test/favicons/browser_favicon_load.js2
-rw-r--r--browser/base/content/test/favicons/browser_favicon_nostore.js13
-rw-r--r--browser/base/content/test/favicons/browser_favicon_referer.js4
-rw-r--r--browser/base/content/test/favicons/browser_missing_favicon.js2
-rw-r--r--browser/base/content/test/forms/browser_selectpopup.js4
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_colors.js2
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_dir.js2
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_large.js2
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_minFontSize.js2
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_text_transform.js2
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_user_input.js2
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_width.js2
-rw-r--r--browser/base/content/test/forms/browser_selectpopup_xhtml.js2
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_context_menu.js4
-rw-r--r--browser/base/content/test/fullscreen/browser_fullscreen_window_focus.js2
-rw-r--r--browser/base/content/test/fullscreen/head.js2
-rw-r--r--browser/base/content/test/general/browser.toml15
-rw-r--r--browser/base/content/test/general/browser_accesskeys.js4
-rw-r--r--browser/base/content/test/general/browser_alltabslistener.js15
-rw-r--r--browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js4
-rw-r--r--browser/base/content/test/general/browser_bug356571.js2
-rw-r--r--browser/base/content/test/general/browser_bug417483.js2
-rw-r--r--browser/base/content/test/general/browser_bug537013.js2
-rw-r--r--browser/base/content/test/general/browser_bug565575.js2
-rw-r--r--browser/base/content/test/general/browser_bug567306.js2
-rw-r--r--browser/base/content/test/general/browser_bug609700.js6
-rw-r--r--browser/base/content/test/general/browser_bug623893.js2
-rw-r--r--browser/base/content/test/general/browser_bug676619.js12
-rw-r--r--browser/base/content/test/general/browser_bug734076.js6
-rw-r--r--browser/base/content/test/general/browser_bug763468_perwindowpb.js2
-rw-r--r--browser/base/content/test/general/browser_bug767836_perwindowpb.js2
-rw-r--r--browser/base/content/test/general/browser_bug817947.js2
-rw-r--r--browser/base/content/test/general/browser_clipboard.js6
-rw-r--r--browser/base/content/test/general/browser_clipboard_pastefile.js4
-rw-r--r--browser/base/content/test/general/browser_documentnavigation.js4
-rw-r--r--browser/base/content/test/general/browser_domFullscreen_fullscreenMode.js6
-rw-r--r--browser/base/content/test/general/browser_double_close_tab.js2
-rw-r--r--browser/base/content/test/general/browser_focusonkeydown.js2
-rw-r--r--browser/base/content/test/general/browser_fullscreen-window-open.js8
-rw-r--r--browser/base/content/test/general/browser_invalid_uri_back_forward_manipulation.js2
-rw-r--r--browser/base/content/test/general/browser_newWindowDrop.js2
-rw-r--r--browser/base/content/test/general/browser_plainTextLinks.js2
-rw-r--r--browser/base/content/test/general/browser_private_no_prompt.js4
-rw-r--r--browser/base/content/test/general/browser_remoteTroubleshoot.js4
-rw-r--r--browser/base/content/test/general/browser_save_link-perwindowpb.js4
-rw-r--r--browser/base/content/test/general/browser_save_link_when_window_navigates.js6
-rw-r--r--browser/base/content/test/general/browser_save_private_link_perwindowpb.js6
-rw-r--r--browser/base/content/test/general/browser_save_video.js2
-rw-r--r--browser/base/content/test/general/browser_tabfocus.js4
-rw-r--r--browser/base/content/test/general/browser_tabs_owner.js6
-rw-r--r--browser/base/content/test/general/browser_viewSourceInTabOnViewSource.js6
-rw-r--r--browser/base/content/test/general/browser_zbug569342.js2
-rw-r--r--browser/base/content/test/general/download_page.html12
-rw-r--r--browser/base/content/test/general/head.js4
-rw-r--r--browser/base/content/test/general/video.oggbin285310 -> 0 bytes
-rw-r--r--browser/base/content/test/general/video.webmbin0 -> 222879 bytes
-rw-r--r--browser/base/content/test/general/web_video.html2
-rw-r--r--browser/base/content/test/general/web_video1.ogvbin28942 -> 0 bytes
-rw-r--r--browser/base/content/test/general/web_video1.ogv^headers^3
-rw-r--r--browser/base/content/test/general/web_video1.webmbin0 -> 17555 bytes
-rw-r--r--browser/base/content/test/general/web_video1.webm^headers^3
-rw-r--r--browser/base/content/test/historySwipeAnimation/browser_historySwipeAnimation.js2
-rw-r--r--browser/base/content/test/keyboard/browser_toolbarButtonKeyPress.js93
-rw-r--r--browser/base/content/test/metaTags/browser_bad_meta_tags.js11
-rw-r--r--browser/base/content/test/metaTags/browser_meta_tags.js8
-rw-r--r--browser/base/content/test/outOfProcess/browser_basic_outofprocess.js9
-rw-r--r--browser/base/content/test/pageActions/head.js2
-rw-r--r--browser/base/content/test/pageinfo/browser.toml2
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_firstPartyIsolation.js2
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_iframe_media.js2
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_image_info.js2
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_images.js4
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_permissions.js12
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_rtl.js25
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_security.js21
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_separate_private.js4
-rw-r--r--browser/base/content/test/pageinfo/browser_pageinfo_svg_image.js2
-rw-r--r--browser/base/content/test/pageinfo/image.html2
-rw-r--r--browser/base/content/test/performance/StartupContentSubframe.sys.mjs2
-rw-r--r--browser/base/content/test/performance/browser_preferences_usage.js19
-rw-r--r--browser/base/content/test/performance/browser_startup_content.js10
-rw-r--r--browser/base/content/test/performance/browser_startup_mainthreadio.js1
-rw-r--r--browser/base/content/test/performance/browser_tabdetach.js4
-rw-r--r--browser/base/content/test/performance/browser_tabopen.js2
-rw-r--r--browser/base/content/test/performance/browser_tabopen_squeeze.js2
-rw-r--r--browser/base/content/test/performance/browser_tabstrip_overflow_underflow.js4
-rw-r--r--browser/base/content/test/performance/browser_tabswitch.js14
-rw-r--r--browser/base/content/test/performance/browser_windowclose.js2
-rw-r--r--browser/base/content/test/performance/browser_windowopen.js2
-rw-r--r--browser/base/content/test/performance/head.js21
-rw-r--r--browser/base/content/test/permissions/browser_autoplay_blocked.js2
-rw-r--r--browser/base/content/test/permissions/browser_canvas_fingerprinting_resistance.js8
-rw-r--r--browser/base/content/test/permissions/browser_site_scoped_permissions.js89
-rw-r--r--browser/base/content/test/permissions/browser_temporary_permissions_navigation.js2
-rw-r--r--browser/base/content/test/plugins/head.js4
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification.js22
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_2.js20
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_3.js12
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_4.js10
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_5.js14
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_accesskey.js2
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_keyboard.js6
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_no_anchors.js6
-rw-r--r--browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js414
-rw-r--r--browser/base/content/test/popups/browser_popup_close_main_window.js6
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI.js2
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_cookies_subview.js2
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_fetch.js2
-rw-r--r--browser/base/content/test/protectionsUI/browser_protectionsUI_info_message.js4
-rw-r--r--browser/base/content/test/referrer/head.js2
-rw-r--r--browser/base/content/test/sanitize/browser_cookiePermission_aboutURL.js4
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-timespans.js2
-rw-r--r--browser/base/content/test/sanitize/browser_sanitize-timespans_v2.js2
-rw-r--r--browser/base/content/test/sanitize/browser_sanitizeDialog_v2.js4
-rw-r--r--browser/base/content/test/sanitize/browser_sanitizeDialog_v2_dataSizes.js65
-rw-r--r--browser/base/content/test/sanitize/head.js4
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_adopt.js8
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_app_locale_changed.js6
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_keys.js16
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_move.js28
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_persist.js4
-rw-r--r--browser/base/content/test/sidebar/browser_sidebar_switcher.js43
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityBlock_focus.js2
-rw-r--r--browser/base/content/test/siteIdentity/browser_identityPopup_clearSiteData.js2
-rw-r--r--browser/base/content/test/siteIdentity/browser_navigation_failures.js4
-rw-r--r--browser/base/content/test/siteIdentity/browser_secure_transport_insecure_scheme.js8
-rw-r--r--browser/base/content/test/siteIdentity/head.js12
-rw-r--r--browser/base/content/test/static/browser_all_files_referenced.js36
-rw-r--r--browser/base/content/test/static/browser_parsable_css.js20
-rw-r--r--browser/base/content/test/static/browser_parsable_script.js9
-rw-r--r--browser/base/content/test/static/browser_sentence_case_strings.js2
-rw-r--r--browser/base/content/test/static/head.js2
-rw-r--r--browser/base/content/test/sync/browser_contextmenu_sendpage.js57
-rw-r--r--browser/base/content/test/sync/browser_contextmenu_sendtab.js2
-rw-r--r--browser/base/content/test/sync/browser_fxa_web_channel.js16
-rw-r--r--browser/base/content/test/sync/browser_sync.js14
-rw-r--r--browser/base/content/test/tabPrompts/browser.toml2
-rw-r--r--browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js8
-rw-r--r--browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js4
-rw-r--r--browser/base/content/test/tabPrompts/browser_contentOrigins.js6
-rw-r--r--browser/base/content/test/tabPrompts/browser_promptDelays.js113
-rw-r--r--browser/base/content/test/tabPrompts/browser_promptFocus.js3
-rw-r--r--browser/base/content/test/tabPrompts/browser_windowPrompt.js16
-rw-r--r--browser/base/content/test/tabcrashed/browser_aboutRestartRequired.toml10
-rw-r--r--browser/base/content/test/tabcrashed/browser_aboutRestartRequired_noForkServer.toml8
-rw-r--r--browser/base/content/test/tabcrashed/head.js8
-rw-r--r--browser/base/content/test/tabs/browser.toml6
-rw-r--r--browser/base/content/test/tabs/browser_allow_process_switches_despite_related_browser.js2
-rw-r--r--browser/base/content/test/tabs/browser_audioTabIcon.js4
-rw-r--r--browser/base/content/test/tabs/browser_blank_tab_label.js49
-rw-r--r--browser/base/content/test/tabs/browser_e10s_about_page_triggeringprincipal.js4
-rw-r--r--browser/base/content/test/tabs/browser_e10s_about_process.js4
-rw-r--r--browser/base/content/test/tabs/browser_lastSeenActive.js260
-rw-r--r--browser/base/content/test/tabs/browser_lazy_tab_browser_events.js14
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js4
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js2
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js2
-rw-r--r--browser/base/content/test/tabs/browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js4
-rw-r--r--browser/base/content/test/tabs/browser_long_data_url_label_truncation.js2
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_close_duplicate_tabs.js178
-rw-r--r--browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js16
-rw-r--r--browser/base/content/test/tabs/browser_new_tab_bookmarks_toolbar_height.js4
-rw-r--r--browser/base/content/test/tabs/browser_new_tab_in_privilegedabout_process_pref.js2
-rw-r--r--browser/base/content/test/tabs/browser_new_tab_url.js4
-rw-r--r--browser/base/content/test/tabs/browser_open_newtab_start_observer_notification.js4
-rw-r--r--browser/base/content/test/tabs/browser_pinnedTabs_closeByKeyboard.js4
-rw-r--r--browser/base/content/test/tabs/browser_privilegedmozilla_process_pref.js2
-rw-r--r--browser/base/content/test/tabs/browser_removeTabs_order.js2
-rw-r--r--browser/base/content/test/tabs/browser_tab_label_picture_in_picture.js2
-rw-r--r--browser/base/content/test/tabs/browser_tab_manager_visibility.js4
-rw-r--r--browser/base/content/test/tabs/browser_tab_preview.js263
-rw-r--r--browser/base/content/test/tabs/browser_tab_tooltips.js2
-rw-r--r--browser/base/content/test/tabs/browser_tabswitch_select.js4
-rw-r--r--browser/base/content/test/tabs/browser_tabswitch_updatecommands.js2
-rw-r--r--browser/base/content/test/tabs/browser_viewsource_of_data_URI_in_file_process.js2
-rw-r--r--browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js19
-rw-r--r--browser/base/content/test/tabs/browser_window_open_modifiers.js2
-rw-r--r--browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js2
-rw-r--r--browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js2
-rw-r--r--browser/base/content/test/webextensions/browser_extension_sideloading.js10
-rw-r--r--browser/base/content/test/webextensions/browser_extension_update_background.js6
-rw-r--r--browser/base/content/test/webextensions/browser_extension_update_background_noprompt.js2
-rw-r--r--browser/base/content/test/webextensions/browser_legacy_webext.xpibin4243 -> 362 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_installTrigger.js6
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_local_file.js17
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_mozAddonManager.js13
-rw-r--r--browser/base/content/test/webextensions/browser_permissions_pointerevent.js6
-rw-r--r--browser/base/content/test/webextensions/browser_update_checkForUpdates.js2
-rw-r--r--browser/base/content/test/webextensions/browser_update_interactive_noprompt.js2
-rw-r--r--browser/base/content/test/webextensions/browser_webext_nopermissions.xpibin4273 -> 7500 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_permissions.xpibin16602 -> 19923 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update1.xpibin4271 -> 326 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update2.xpibin4291 -> 343 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_icon1.xpibin16545 -> 12581 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_icon2.xpibin16564 -> 12599 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_perms1.xpibin4273 -> 320 bytes
-rw-r--r--browser/base/content/test/webextensions/browser_webext_update_perms2.xpibin4282 -> 331 bytes
-rw-r--r--browser/base/content/test/webextensions/head.js4
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_anim.js4
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_by_device_id.js2
-rw-r--r--browser/base/content/test/webrtc/browser_devices_get_user_media_grace.js6
-rw-r--r--browser/base/content/test/webrtc/browser_devices_select_audio_output.js2
-rw-r--r--browser/base/content/test/webrtc/browser_webrtc_hooks.js28
-rw-r--r--browser/base/content/test/webrtc/head.js6
-rw-r--r--browser/base/content/test/zoom/browser.toml2
-rw-r--r--browser/base/content/test/zoom/browser_sitespecific_video_zoom.js2
-rw-r--r--browser/base/content/test/zoom/browser_zoom_commands.js4
-rw-r--r--browser/base/content/test/zoom/head.js8
-rw-r--r--browser/base/content/titlebar-items.inc.xhtml2
-rw-r--r--browser/base/content/utilityOverlay.js9
-rw-r--r--browser/base/content/webext-panels.js12
-rw-r--r--browser/base/content/webext-panels.xhtml2
-rw-r--r--browser/base/content/webrtcIndicator.js2
-rw-r--r--browser/base/jar.mn4
-rw-r--r--browser/base/triage.json51
-rw-r--r--browser/branding/aurora/pref/firefox-branding.js2
-rw-r--r--browser/branding/official/pref/firefox-branding.js2
-rw-r--r--browser/components/BrowserContentHandler.sys.mjs148
-rw-r--r--browser/components/BrowserGlue.sys.mjs314
-rw-r--r--browser/components/StartupRecorder.sys.mjs26
-rw-r--r--browser/components/aboutlogins/AboutLoginsChild.sys.mjs6
-rw-r--r--browser/components/aboutlogins/AboutLoginsParent.sys.mjs21
-rw-r--r--browser/components/aboutlogins/LoginBreaches.sys.mjs23
-rw-r--r--browser/components/aboutlogins/content/aboutLogins.html1
-rw-r--r--browser/components/aboutlogins/content/aboutLoginsImportReport.html1
-rw-r--r--browser/components/aboutlogins/content/components/login-item.css21
-rw-r--r--browser/components/aboutlogins/tests/browser/browser.toml1
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js14
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js11
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js6
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js6
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_createLogin.js9
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_deleteLogin.js5
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js5
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_openExport.js3
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_openSite.js5
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js348
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js12
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_sessionRestore.js2
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_tabKeyNav.js15
-rw-r--r--browser/components/aboutlogins/tests/browser/browser_updateLogin.js37
-rw-r--r--browser/components/aboutlogins/tests/browser/head.js35
-rw-r--r--browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html5
-rw-r--r--browser/components/aboutwelcome/.eslintrc.js2
-rw-r--r--browser/components/aboutwelcome/actors/AboutWelcomeParent.sys.mjs2
-rw-r--r--browser/components/aboutwelcome/content-src/aboutwelcome.scss139
-rw-r--r--browser/components/aboutwelcome/content-src/components/MultiStageAboutWelcome.jsx16
-rw-r--r--browser/components/aboutwelcome/content-src/components/MultiStageProtonScreen.jsx55
-rw-r--r--browser/components/aboutwelcome/content-src/components/Themes.jsx9
-rw-r--r--browser/components/aboutwelcome/content/aboutwelcome.bundle.js36
-rw-r--r--browser/components/aboutwelcome/content/aboutwelcome.css108
-rw-r--r--browser/components/aboutwelcome/content/aboutwelcome.html1
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_configurable_ui.js2
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_mr.js2
-rw-r--r--browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_toolbar_button.js2
-rw-r--r--browser/components/aboutwelcome/tests/unit/MultiStageAWProton.test.jsx23
-rw-r--r--browser/components/aboutwelcome/tests/unit/MultiStageAboutWelcome.test.jsx66
-rw-r--r--browser/components/aboutwelcome/tests/unit/unit-entry.js14
-rw-r--r--browser/components/asrouter/.eslintrc.js2
-rw-r--r--browser/components/asrouter/actors/ASRouterChild.sys.mjs6
-rw-r--r--browser/components/asrouter/bin/import-rollouts.js4
-rw-r--r--browser/components/asrouter/content-src/asrouter-utils.mjs6
-rw-r--r--browser/components/asrouter/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx16
-rw-r--r--browser/components/asrouter/content-src/schemas/BackgroundTaskMessagingExperiment.schema.json69
-rw-r--r--browser/components/asrouter/content-src/schemas/MessagingExperiment.schema.json415
-rwxr-xr-xbrowser/components/asrouter/content-src/schemas/make-schemas.py3
-rw-r--r--browser/components/asrouter/content-src/styles/_feature-callout.scss2
-rw-r--r--browser/components/asrouter/content-src/templates/OnboardingMessage/WhatsNewMessage.schema.json73
-rw-r--r--browser/components/asrouter/content/asrouter-admin.bundle.js57
-rw-r--r--browser/components/asrouter/content/components/ASRouterAdmin/ASRouterAdmin.css21
-rw-r--r--browser/components/asrouter/docs/targeting-attributes.md21
-rw-r--r--browser/components/asrouter/modules/ASRouter.sys.mjs47
-rw-r--r--browser/components/asrouter/modules/ASRouterParentProcessMessageHandler.sys.mjs9
-rw-r--r--browser/components/asrouter/modules/ASRouterTargeting.sys.mjs32
-rw-r--r--browser/components/asrouter/modules/ActorConstants.mjs (renamed from browser/components/asrouter/modules/ActorConstants.sys.mjs)3
-rw-r--r--browser/components/asrouter/modules/CFRMessageProvider.sys.mjs68
-rw-r--r--browser/components/asrouter/modules/FeatureCallout.sys.mjs54
-rw-r--r--browser/components/asrouter/modules/FeatureCalloutMessages.sys.mjs6
-rw-r--r--browser/components/asrouter/modules/MomentsPageHub.sys.mjs2
-rw-r--r--browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs113
-rw-r--r--browser/components/asrouter/modules/PanelTestProvider.sys.mjs130
-rw-r--r--browser/components/asrouter/modules/RemoteL10n.sys.mjs1
-rw-r--r--browser/components/asrouter/modules/ToolbarBadgeHub.sys.mjs36
-rw-r--r--browser/components/asrouter/modules/ToolbarPanelHub.sys.mjs544
-rw-r--r--browser/components/asrouter/moz.build4
-rw-r--r--browser/components/asrouter/package.json1
-rw-r--r--browser/components/asrouter/tests/InflightAssetsMessageProvider.sys.mjs112
-rw-r--r--browser/components/asrouter/tests/NimbusRolloutMessageProvider.sys.mjs421
-rw-r--r--browser/components/asrouter/tests/browser/browser.toml5
-rw-r--r--browser/components/asrouter/tests/browser/browser_asrouter_bug1761522.js2
-rw-r--r--browser/components/asrouter/tests/browser/browser_asrouter_cfr.js117
-rw-r--r--browser/components/asrouter/tests/browser/browser_asrouter_infobar.js8
-rw-r--r--browser/components/asrouter/tests/browser/browser_asrouter_keyboard_cfr.js162
-rw-r--r--browser/components/asrouter/tests/browser/browser_asrouter_milestone_message_cfr.js78
-rw-r--r--browser/components/asrouter/tests/browser/browser_asrouter_momentspagehub.js4
-rw-r--r--browser/components/asrouter/tests/browser/browser_asrouter_targeting.js63
-rw-r--r--browser/components/asrouter/tests/unit/ASRouter.test.js107
-rw-r--r--browser/components/asrouter/tests/unit/ASRouterChild.test.js3
-rw-r--r--browser/components/asrouter/tests/unit/ASRouterParent.test.js2
-rw-r--r--browser/components/asrouter/tests/unit/ASRouterParentProcessMessageHandler.test.js27
-rw-r--r--browser/components/asrouter/tests/unit/CFRMessageProvider.test.js25
-rw-r--r--browser/components/asrouter/tests/unit/RemoteL10n.test.js2
-rw-r--r--browser/components/asrouter/tests/unit/ToolbarBadgeHub.test.js166
-rw-r--r--browser/components/asrouter/tests/unit/ToolbarPanelHub.test.js760
-rw-r--r--browser/components/asrouter/tests/unit/content-src/components/ASRouterAdmin.test.jsx44
-rw-r--r--browser/components/asrouter/tests/unit/unit-entry.js2
-rw-r--r--browser/components/asrouter/tests/xpcshell/head.js4
-rw-r--r--browser/components/asrouter/tests/xpcshell/test_PanelTestProvider.js1
-rw-r--r--browser/components/asrouter/yamscripts.yml1
-rw-r--r--browser/components/backup/.eslintrc.js9
-rw-r--r--browser/components/backup/BackupResources.sys.mjs24
-rw-r--r--browser/components/backup/BackupService.sys.mjs696
-rw-r--r--browser/components/backup/actors/BackupUIChild.sys.mjs54
-rw-r--r--browser/components/backup/actors/BackupUIParent.sys.mjs82
-rw-r--r--browser/components/backup/content/BackupManifest.1.schema.json82
-rw-r--r--browser/components/backup/content/backup-settings.mjs47
-rw-r--r--browser/components/backup/content/backup-settings.stories.mjs32
-rw-r--r--browser/components/backup/content/debug.html64
-rw-r--r--browser/components/backup/content/debug.js113
-rw-r--r--browser/components/backup/docs/backup-resources.rst18
-rw-r--r--browser/components/backup/docs/backup-ui-actors.rst22
-rw-r--r--browser/components/backup/docs/index.rst2
-rw-r--r--browser/components/backup/jar.mn11
-rw-r--r--browser/components/backup/metrics.yaml276
-rw-r--r--browser/components/backup/moz.build18
-rw-r--r--browser/components/backup/resources/AddonsBackupResource.sys.mjs167
-rw-r--r--browser/components/backup/resources/BackupResource.sys.mjs241
-rw-r--r--browser/components/backup/resources/CookiesBackupResource.sys.mjs39
-rw-r--r--browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs89
-rw-r--r--browser/components/backup/resources/FormHistoryBackupResource.sys.mjs41
-rw-r--r--browser/components/backup/resources/MiscDataBackupResource.sys.mjs154
-rw-r--r--browser/components/backup/resources/PlacesBackupResource.sys.mjs130
-rw-r--r--browser/components/backup/resources/PreferencesBackupResource.sys.mjs100
-rw-r--r--browser/components/backup/resources/SessionStoreBackupResource.sys.mjs79
-rw-r--r--browser/components/backup/tests/browser/browser.toml7
-rw-r--r--browser/components/backup/tests/browser/browser_settings.js40
-rw-r--r--browser/components/backup/tests/chrome/chrome.toml4
-rw-r--r--browser/components/backup/tests/chrome/test_backup_settings.html43
-rw-r--r--browser/components/backup/tests/marionette/http2-ca.pem18
-rw-r--r--browser/components/backup/tests/marionette/manifest.toml6
-rw-r--r--browser/components/backup/tests/marionette/test_backup.py713
-rw-r--r--browser/components/backup/tests/xpcshell/data/test_xulstore.json1
-rw-r--r--browser/components/backup/tests/xpcshell/head.js173
-rw-r--r--browser/components/backup/tests/xpcshell/test_AddonsBackupResource.js416
-rw-r--r--browser/components/backup/tests/xpcshell/test_BackupResource.js250
-rw-r--r--browser/components/backup/tests/xpcshell/test_BackupService.js451
-rw-r--r--browser/components/backup/tests/xpcshell/test_BackupService_takeMeasurements.js59
-rw-r--r--browser/components/backup/tests/xpcshell/test_BrowserResource.js63
-rw-r--r--browser/components/backup/tests/xpcshell/test_CookiesBackupResource.js142
-rw-r--r--browser/components/backup/tests/xpcshell/test_CredentialsAndSecurityBackupResource.js215
-rw-r--r--browser/components/backup/tests/xpcshell/test_FormHistoryBackupResource.js146
-rw-r--r--browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js302
-rw-r--r--browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js369
-rw-r--r--browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js196
-rw-r--r--browser/components/backup/tests/xpcshell/test_SessionStoreBackupResource.js209
-rw-r--r--browser/components/backup/tests/xpcshell/test_measurements.js40
-rw-r--r--browser/components/backup/tests/xpcshell/xpcshell.toml26
-rw-r--r--browser/components/contentanalysis/content/ContentAnalysis.sys.mjs206
-rw-r--r--browser/components/contextualidentity/content/usercontext.css8
-rw-r--r--browser/components/contextualidentity/test/browser/browser_eme.js2
-rw-r--r--browser/components/contextualidentity/test/browser/browser_favicon.js2
-rw-r--r--browser/components/contextualidentity/test/browser/browser_forgetAPI_EME_forgetThisSite.js2
-rw-r--r--browser/components/contextualidentity/test/browser/browser_forgetaboutsite.js6
-rw-r--r--browser/components/contextualidentity/test/browser/browser_guessusercontext.js2
-rw-r--r--browser/components/contextualidentity/test/browser/browser_middleClick.js2
-rw-r--r--browser/components/contextualidentity/test/browser/browser_serviceworkers.js2
-rw-r--r--browser/components/contextualidentity/test/browser/browser_windowName.js4
-rw-r--r--browser/components/contextualidentity/test/browser/file_set_storages.html2
-rw-r--r--browser/components/controlcenter/content/protectionsPanel.inc.xhtml4
-rw-r--r--browser/components/customizableui/CustomizableUI.sys.mjs4
-rw-r--r--browser/components/customizableui/CustomizableWidgets.sys.mjs74
-rw-r--r--browser/components/customizableui/CustomizeMode.sys.mjs22
-rw-r--r--browser/components/customizableui/content/panelUI.inc.xhtml4
-rw-r--r--browser/components/customizableui/content/panelUI.js123
-rw-r--r--browser/components/customizableui/test/browser_1087303_button_fullscreen.js2
-rw-r--r--browser/components/customizableui/test/browser_1087303_button_preferences.js2
-rw-r--r--browser/components/customizableui/test/browser_1484275_PanelMultiView_toggle_with_other_popup.js2
-rw-r--r--browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js4
-rw-r--r--browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js7
-rw-r--r--browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js2
-rw-r--r--browser/components/customizableui/test/browser_947914_button_newWindow.js2
-rw-r--r--browser/components/customizableui/test/browser_947914_button_zoomReset.js2
-rw-r--r--browser/components/customizableui/test/browser_972267_customizationchange_events.js2
-rw-r--r--browser/components/customizableui/test/browser_PanelMultiView_keyboard.js1
-rw-r--r--browser/components/customizableui/test/browser_ctrl_click_panel_opening.js4
-rw-r--r--browser/components/customizableui/test/browser_customization_context_menus.js10
-rw-r--r--browser/components/customizableui/test/browser_editcontrols_update.js2
-rw-r--r--browser/components/customizableui/test/browser_open_in_lazy_tab.js2
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications.js12
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications_bannerVisibility.js6
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications_fullscreen_noAutoHideToolbar.js2
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications_modals.js4
-rw-r--r--browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js6
-rw-r--r--browser/components/customizableui/test/browser_sidebar_toggle.js22
-rw-r--r--browser/components/customizableui/test/browser_switch_to_customize_mode.js2
-rw-r--r--browser/components/customizableui/test/browser_synced_tabs_menu.js57
-rw-r--r--browser/components/customizableui/test/head.js12
-rw-r--r--browser/components/distribution.sys.mjs17
-rw-r--r--browser/components/doh/DoHConfig.sys.mjs2
-rw-r--r--browser/components/doh/test/browser/browser_remoteSettings_newProfile.js2
-rw-r--r--browser/components/downloads/content/allDownloadsView.js2
-rw-r--r--browser/components/downloads/content/downloads.js6
-rw-r--r--browser/components/enterprisepolicies/Policies.sys.mjs159
-rw-r--r--browser/components/enterprisepolicies/content/aboutPolicies.html1
-rw-r--r--browser/components/enterprisepolicies/content/aboutPolicies.js1
-rw-r--r--browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs48
-rw-r--r--browser/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs24
-rw-r--r--browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs4
-rw-r--r--browser/components/enterprisepolicies/schemas/policies-schema.json42
-rw-r--r--browser/components/enterprisepolicies/tests/browser/browser.toml2
-rw-r--r--browser/components/enterprisepolicies/tests/browser/browser_policy_extensions.js2
-rw-r--r--browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js2
-rw-r--r--browser/components/enterprisepolicies/tests/browser/browser_policy_masterpassword.js19
-rw-r--r--browser/components/enterprisepolicies/tests/browser/browser_policy_pageinfo_permissions.js4
-rw-r--r--browser/components/enterprisepolicies/tests/browser/browser_policy_translateenabled.js53
-rw-r--r--browser/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js2
-rw-r--r--browser/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js2
-rw-r--r--browser/components/enterprisepolicies/tests/browser/head.js2
-rw-r--r--browser/components/enterprisepolicies/tests/xpcshell/head.js2
-rw-r--r--browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js159
-rw-r--r--browser/components/enterprisepolicies/tests/xpcshell/test_permissions.js21
-rw-r--r--browser/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js6
-rw-r--r--browser/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js105
-rw-r--r--browser/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js2
-rw-r--r--browser/components/enterprisepolicies/tests/xpcshell/xpcshell.toml5
-rw-r--r--browser/components/extensions/ExtensionControlledPopup.sys.mjs2
-rw-r--r--browser/components/extensions/extension.css2
-rw-r--r--browser/components/extensions/parent/ext-browser.js27
-rw-r--r--browser/components/extensions/parent/ext-chrome-settings-overrides.js2
-rw-r--r--browser/components/extensions/parent/ext-commands.js7
-rw-r--r--browser/components/extensions/parent/ext-devtools-panels.js42
-rw-r--r--browser/components/extensions/parent/ext-menus.js14
-rw-r--r--browser/components/extensions/parent/ext-sidebarAction.js169
-rw-r--r--browser/components/extensions/parent/ext-tabs.js8
-rw-r--r--browser/components/extensions/schemas/commands.json6
-rw-r--r--browser/components/extensions/schemas/tabs.json4
-rw-r--r--browser/components/extensions/test/browser/browser.toml12
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_context.js6
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js123
-rw-r--r--browser/components/extensions/test/browser/browser_ext_browserAction_popup_preload_smoketest.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_chrome_settings_overrides_home.js10
-rw-r--r--browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js236
-rw-r--r--browser/components/extensions/test/browser/browser_ext_commands_onCommand.js14
-rw-r--r--browser/components/extensions/test/browser/browser_ext_commands_update.js8
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contentscript_nontab_connect.js6
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contentscript_sender_url.js21
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus_bookmarks.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js6
-rw-r--r--browser/components/extensions/test/browser/browser_ext_contextMenus_targetUrlPatterns.js18
-rw-r--r--browser/components/extensions/test/browser/browser_ext_getViews.js38
-rw-r--r--browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js8
-rw-r--r--browser/components/extensions/test/browser/browser_ext_menus_targetElement.js8
-rw-r--r--browser/components/extensions/test/browser/browser_ext_mousewheel_zoom.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_openPanel.js6
-rw-r--r--browser/components/extensions/test/browser/browser_ext_originControls.js6
-rw-r--r--browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js18
-rw-r--r--browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_runtime_getContexts.js597
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js16
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js6
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sidebarAction.js60
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js7
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js9
-rw-r--r--browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js2
-rw-r--r--browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js2
-rw-r--r--browser/components/extensions/test/browser/browser_unified_extensions.js7
-rw-r--r--browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js6
-rw-r--r--browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js108
-rw-r--r--browser/components/extensions/test/browser/head.js52
-rw-r--r--browser/components/firefoxview/HistoryController.sys.mjs383
-rw-r--r--browser/components/firefoxview/OpenTabs.sys.mjs55
-rw-r--r--browser/components/firefoxview/SyncedTabsController.sys.mjs333
-rw-r--r--browser/components/firefoxview/card-container.css6
-rw-r--r--browser/components/firefoxview/card-container.mjs133
-rw-r--r--browser/components/firefoxview/firefox-view-notification-manager.sys.mjs112
-rw-r--r--browser/components/firefoxview/firefox-view-places-query.sys.mjs187
-rw-r--r--browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs17
-rw-r--r--browser/components/firefoxview/firefoxview.css8
-rw-r--r--browser/components/firefoxview/firefoxview.html13
-rw-r--r--browser/components/firefoxview/firefoxview.mjs16
-rw-r--r--browser/components/firefoxview/fxview-empty-state.css2
-rw-r--r--browser/components/firefoxview/fxview-empty-state.mjs31
-rw-r--r--browser/components/firefoxview/fxview-search-textbox.mjs2
-rw-r--r--browser/components/firefoxview/fxview-tab-list.css22
-rw-r--r--browser/components/firefoxview/fxview-tab-list.mjs685
-rw-r--r--browser/components/firefoxview/fxview-tab-row.css178
-rw-r--r--browser/components/firefoxview/helpers.mjs38
-rw-r--r--browser/components/firefoxview/history.css13
-rw-r--r--browser/components/firefoxview/history.mjs247
-rw-r--r--browser/components/firefoxview/jar.mn4
-rw-r--r--browser/components/firefoxview/opentabs-tab-list.css32
-rw-r--r--browser/components/firefoxview/opentabs-tab-list.mjs593
-rw-r--r--browser/components/firefoxview/opentabs-tab-row.css119
-rw-r--r--browser/components/firefoxview/opentabs.mjs44
-rw-r--r--browser/components/firefoxview/recentlyclosed.mjs23
-rw-r--r--browser/components/firefoxview/search-helpers.mjs24
-rw-r--r--browser/components/firefoxview/syncedtabs.mjs388
-rw-r--r--browser/components/firefoxview/tests/browser/browser.toml11
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview.js39
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js102
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js99
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js5
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js6
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js4
-rw-r--r--browser/components/firefoxview/tests/browser/browser_history_firefoxview.js84
-rw-r--r--browser/components/firefoxview/tests/browser/browser_notification_dot.js392
-rw-r--r--browser/components/firefoxview/tests/browser/browser_opentabs_cards.js10
-rw-r--r--browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js4
-rw-r--r--browser/components/firefoxview/tests/browser/browser_opentabs_recency.js350
-rw-r--r--browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js10
-rw-r--r--browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js12
-rw-r--r--browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js272
-rw-r--r--browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js167
-rw-r--r--browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js4
-rw-r--r--browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js2
-rw-r--r--browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html26
-rw-r--r--browser/components/ion/content/ion.js12
-rw-r--r--browser/components/ion/test/browser/browser_ion_ui.js35
-rw-r--r--browser/components/messagepreview/limelight.svg2
-rw-r--r--browser/components/messagepreview/messagepreview.js14
-rw-r--r--browser/components/migration/.eslintrc.js5
-rw-r--r--browser/components/migration/ChromeProfileMigrator.sys.mjs3
-rw-r--r--browser/components/migration/FileMigrators.sys.mjs5
-rw-r--r--browser/components/migration/MSMigrationUtils.sys.mjs2
-rw-r--r--browser/components/migration/MigrationUtils.sys.mjs49
-rw-r--r--browser/components/migration/MigratorBase.sys.mjs10
-rw-r--r--browser/components/migration/SafariProfileMigrator.sys.mjs4
-rw-r--r--browser/components/migration/content/migration-wizard.mjs215
-rw-r--r--browser/components/migration/tests/browser/browser_disabled_migrator.js4
-rw-r--r--browser/components/migration/tests/browser/browser_do_migration.js2
-rw-r--r--browser/components/migration/tests/browser/browser_file_migration.js4
-rw-r--r--browser/components/migration/tests/browser/head.js2
-rw-r--r--browser/components/migration/tests/chrome/test_migration_wizard.html4
-rw-r--r--browser/components/migration/tests/unit/head_migration.js28
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_bookmarks.js20
-rw-r--r--browser/components/migration/tests/unit/test_Safari_history_strange_entries.js7
-rw-r--r--browser/components/moz.build1
-rw-r--r--browser/components/newtab/.eslintrc.js8
-rw-r--r--browser/components/newtab/AboutNewTabService.sys.mjs6
-rw-r--r--browser/components/newtab/common/Actions.mjs (renamed from browser/components/newtab/common/Actions.sys.mjs)15
-rw-r--r--browser/components/newtab/common/Reducers.sys.mjs36
-rw-r--r--browser/components/newtab/content-src/activity-stream.jsx5
-rw-r--r--browser/components/newtab/content-src/components/Base/Base.jsx172
-rw-r--r--browser/components/newtab/content-src/components/Base/_Base.scss38
-rw-r--r--browser/components/newtab/content-src/components/Card/Card.jsx5
-rw-r--r--browser/components/newtab/content-src/components/Card/types.mjs (renamed from browser/components/newtab/content-src/components/Card/types.js)0
-rw-r--r--browser/components/newtab/content-src/components/CollapsibleSection/CollapsibleSection.jsx2
-rw-r--r--browser/components/newtab/content-src/components/ComponentPerfTimer/ComponentPerfTimer.jsx5
-rw-r--r--browser/components/newtab/content-src/components/ConfirmDialog/ConfirmDialog.jsx2
-rw-r--r--browser/components/newtab/content-src/components/ContextMenu/ContextMenu.jsx4
-rw-r--r--browser/components/newtab/content-src/components/CustomizeMenu/ContentSection/ContentSection.jsx35
-rw-r--r--browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx19
-rw-r--r--browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss19
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx68
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss12
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamAdmin/SimpleHashRouter.jsx8
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx6
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx9
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/CollectionCardGrid/CollectionCardGrid.jsx2
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx88
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss32
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx2
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSEmptyState/DSEmptyState.jsx5
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSLinkMenu/DSLinkMenu.jsx2
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal.jsx5
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSSignup/DSSignup.jsx2
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo.jsx2
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/FeatureHighlight/FeatureHighlight.jsx2
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss18
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/Navigation/Navigation.jsx2
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor.jsx5
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx2
-rw-r--r--browser/components/newtab/content-src/components/DiscoveryStreamImpressionStats/ImpressionStats.jsx11
-rw-r--r--browser/components/newtab/content-src/components/LinkMenu/LinkMenu.jsx2
-rw-r--r--browser/components/newtab/content-src/components/ModalOverlay/ModalOverlay.jsx2
-rw-r--r--browser/components/newtab/content-src/components/Search/Search.jsx5
-rw-r--r--browser/components/newtab/content-src/components/Sections/Sections.jsx9
-rw-r--r--browser/components/newtab/content-src/components/TopSites/SearchShortcutsForm.jsx5
-rw-r--r--browser/components/newtab/content-src/components/TopSites/TopSite.jsx5
-rw-r--r--browser/components/newtab/content-src/components/TopSites/TopSiteForm.jsx5
-rw-r--r--browser/components/newtab/content-src/components/TopSites/TopSiteImpressionWrapper.jsx6
-rw-r--r--browser/components/newtab/content-src/components/TopSites/TopSites.jsx7
-rw-r--r--browser/components/newtab/content-src/components/TopSites/TopSitesConstants.mjs (renamed from browser/components/newtab/content-src/components/TopSites/TopSitesConstants.js)0
-rw-r--r--browser/components/newtab/content-src/components/TopSites/_TopSites.scss28
-rw-r--r--browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx119
-rw-r--r--browser/components/newtab/content-src/components/WallpapersSection/_WallpapersSection.scss87
-rw-r--r--browser/components/newtab/content-src/components/Weather/Weather.jsx350
-rw-r--r--browser/components/newtab/content-src/components/Weather/_Weather.scss393
-rw-r--r--browser/components/newtab/content-src/lib/constants.mjs (renamed from browser/components/newtab/content-src/lib/constants.js)2
-rw-r--r--browser/components/newtab/content-src/lib/detect-user-session-start.mjs (renamed from browser/components/newtab/content-src/lib/detect-user-session-start.js)6
-rw-r--r--browser/components/newtab/content-src/lib/init-store.mjs (renamed from browser/components/newtab/content-src/lib/init-store.js)11
-rw-r--r--browser/components/newtab/content-src/lib/link-menu-options.mjs (renamed from browser/components/newtab/content-src/lib/link-menu-options.js)66
-rw-r--r--browser/components/newtab/content-src/lib/perf-service.mjs (renamed from browser/components/newtab/content-src/lib/perf-service.js)12
-rw-r--r--browser/components/newtab/content-src/lib/screenshot-utils.mjs (renamed from browser/components/newtab/content-src/lib/screenshot-utils.js)4
-rw-r--r--browser/components/newtab/content-src/lib/selectLayoutRender.mjs (renamed from browser/components/newtab/content-src/lib/selectLayoutRender.js)0
-rw-r--r--browser/components/newtab/content-src/styles/_activity-stream.scss13
-rw-r--r--browser/components/newtab/content-src/styles/_icons.scss10
-rw-r--r--browser/components/newtab/content-src/styles/_theme.scss21
-rw-r--r--browser/components/newtab/content-src/styles/_variables.scss27
-rw-r--r--browser/components/newtab/css/activity-stream-linux.css626
-rw-r--r--browser/components/newtab/css/activity-stream-mac.css626
-rw-r--r--browser/components/newtab/css/activity-stream-windows.css626
-rw-r--r--browser/components/newtab/data/content/activity-stream.bundle.js1418
-rw-r--r--browser/components/newtab/data/content/assets/glyph-info-critical-16.svg6
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/dark-beach.avifbin0 -> 4043 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/dark-color.avifbin0 -> 2413 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avifbin0 -> 9381 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avifbin0 -> 11602 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/dark-panda.avifbin0 -> 4606 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/dark-sky.avifbin0 -> 2216 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/light-beach.avifbin0 -> 3806 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/light-color.avifbin0 -> 2267 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/light-landscape.avifbin0 -> 2527 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/light-mountain.avifbin0 -> 5915 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/light-panda.avifbin0 -> 8667 bytes
-rw-r--r--browser/components/newtab/data/content/assets/wallpapers/light-sky.avifbin0 -> 2540 bytes
-rw-r--r--browser/components/newtab/karma.mc.config.js31
-rw-r--r--browser/components/newtab/lib/AboutPreferences.sys.mjs36
-rw-r--r--browser/components/newtab/lib/ActivityStream.sys.mjs91
-rw-r--r--browser/components/newtab/lib/ActivityStreamMessageChannel.sys.mjs2
-rw-r--r--browser/components/newtab/lib/ActivityStreamStorage.sys.mjs9
-rw-r--r--browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs48
-rw-r--r--browser/components/newtab/lib/DownloadsManager.sys.mjs10
-rw-r--r--browser/components/newtab/lib/FaviconFeed.sys.mjs2
-rw-r--r--browser/components/newtab/lib/HighlightsFeed.sys.mjs2
-rw-r--r--browser/components/newtab/lib/NewTabInit.sys.mjs2
-rw-r--r--browser/components/newtab/lib/PlacesFeed.sys.mjs5
-rw-r--r--browser/components/newtab/lib/PrefsFeed.sys.mjs2
-rw-r--r--browser/components/newtab/lib/RecommendationProvider.sys.mjs2
-rw-r--r--browser/components/newtab/lib/SectionsManager.sys.mjs4
-rw-r--r--browser/components/newtab/lib/SystemTickFeed.sys.mjs2
-rw-r--r--browser/components/newtab/lib/TelemetryFeed.sys.mjs128
-rw-r--r--browser/components/newtab/lib/TopSitesFeed.sys.mjs6
-rw-r--r--browser/components/newtab/lib/TopStoriesFeed.sys.mjs2
-rw-r--r--browser/components/newtab/lib/WallpaperFeed.sys.mjs117
-rw-r--r--browser/components/newtab/lib/WeatherFeed.sys.mjs208
-rw-r--r--browser/components/newtab/metrics.yaml160
-rw-r--r--browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js8
-rw-r--r--browser/components/newtab/test/browser/browser_as_load_location.js2
-rw-r--r--browser/components/newtab/test/browser/browser_customize_menu_content.js83
-rw-r--r--browser/components/newtab/test/browser/browser_newtab_overrides.js4
-rw-r--r--browser/components/newtab/test/schemas/pings.js5
-rw-r--r--browser/components/newtab/test/unit/common/Actions.test.js2
-rw-r--r--browser/components/newtab/test/unit/common/Reducers.test.js2
-rw-r--r--browser/components/newtab/test/unit/content-src/components/Base.test.jsx79
-rw-r--r--browser/components/newtab/test/unit/content-src/components/Card.test.jsx5
-rw-r--r--browser/components/newtab/test/unit/content-src/components/ComponentPerfTimer.test.jsx5
-rw-r--r--browser/components/newtab/test/unit/content-src/components/ConfirmDialog.test.jsx5
-rw-r--r--browser/components/newtab/test/unit/content-src/components/CustomiseMenu.test.jsx7
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamAdmin.test.jsx15
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx5
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx26
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx2
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSPrivacyModal.test.jsx2
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx73
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx5
-rw-r--r--browser/components/newtab/test/unit/content-src/components/Sections.test.jsx2
-rw-r--r--browser/components/newtab/test/unit/content-src/components/TopSites.test.jsx5
-rw-r--r--browser/components/newtab/test/unit/content-src/components/TopSites/TopSiteImpressionWrapper.test.jsx2
-rw-r--r--browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js5
-rw-r--r--browser/components/newtab/test/unit/content-src/lib/init-store.test.js5
-rw-r--r--browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js2
-rw-r--r--browser/components/newtab/test/unit/lib/AboutPreferences.test.js20
-rw-r--r--browser/components/newtab/test/unit/lib/ActivityStream.test.js34
-rw-r--r--browser/components/newtab/test/unit/lib/ActivityStreamMessageChannel.test.js7
-rw-r--r--browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js9
-rw-r--r--browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js46
-rw-r--r--browser/components/newtab/test/unit/lib/DownloadsManager.test.js6
-rw-r--r--browser/components/newtab/test/unit/lib/FaviconFeed.test.js2
-rw-r--r--browser/components/newtab/test/unit/lib/NewTabInit.test.js5
-rw-r--r--browser/components/newtab/test/unit/lib/PrefsFeed.test.js5
-rw-r--r--browser/components/newtab/test/unit/lib/RecommendationProvider.test.js5
-rw-r--r--browser/components/newtab/test/unit/lib/SectionsManager.test.js2
-rw-r--r--browser/components/newtab/test/unit/lib/SystemTickFeed.test.js2
-rw-r--r--browser/components/newtab/test/xpcshell/test_HighlightsFeed.js2
-rw-r--r--browser/components/newtab/test/xpcshell/test_PlacesFeed.js8
-rw-r--r--browser/components/newtab/test/xpcshell/test_TelemetryFeed.js56
-rw-r--r--browser/components/newtab/test/xpcshell/test_TopSitesFeed.js5
-rw-r--r--browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js259
-rw-r--r--browser/components/newtab/test/xpcshell/test_WallpaperFeed.js115
-rw-r--r--browser/components/newtab/test/xpcshell/test_WeatherFeed.js99
-rw-r--r--browser/components/newtab/test/xpcshell/xpcshell.toml4
-rw-r--r--browser/components/newtab/webpack.system-addon.config.js2
-rw-r--r--browser/components/originattributes/test/browser/browser.toml2
-rw-r--r--browser/components/originattributes/test/browser/browser_cache.js8
-rw-r--r--browser/components/originattributes/test/browser/browser_favicon_firstParty.js6
-rw-r--r--browser/components/originattributes/test/browser/browser_favicon_userContextId.js4
-rw-r--r--browser/components/originattributes/test/browser/browser_firstPartyIsolation_saveAs.js4
-rw-r--r--browser/components/originattributes/test/browser/browser_httpauth.js6
-rw-r--r--browser/components/originattributes/test/browser/browser_imageCacheIsolation.js4
-rw-r--r--browser/components/originattributes/test/browser/browser_sanitize.js4
-rw-r--r--browser/components/originattributes/test/browser/file_saveAs.sjs4
-rw-r--r--browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogvbin16049 -> 0 bytes
-rw-r--r--browser/components/originattributes/test/browser/file_thirdPartyChild.video.webmbin0 -> 17931 bytes
-rw-r--r--browser/components/originattributes/test/browser/head.js2
-rw-r--r--browser/components/pagedata/.eslintrc.js2
-rw-r--r--browser/components/pagedata/PageDataService.sys.mjs16
-rw-r--r--browser/components/places/.eslintrc.js9
-rw-r--r--browser/components/places/PlacesUIUtils.sys.mjs36
-rw-r--r--browser/components/places/content/controller.js2
-rw-r--r--browser/components/places/content/places.js2
-rw-r--r--browser/components/places/content/places.xhtml4
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js56
-rw-r--r--browser/components/places/tests/browser/browser_bookmarksProperties.js8
-rw-r--r--browser/components/places/tests/browser/browser_check_correct_controllers.js4
-rw-r--r--browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js2
-rw-r--r--browser/components/places/tests/browser/browser_sidebar_on_customization.js6
-rw-r--r--browser/components/places/tests/browser/browser_sidebarpanels_click.js14
-rw-r--r--browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js4
-rw-r--r--browser/components/places/tests/browser/browser_views_iconsupdate.js4
-rw-r--r--browser/components/places/tests/browser/head.js6
-rw-r--r--browser/components/pocket/content/SaveToPocket.sys.mjs2
-rw-r--r--browser/components/pocket/content/panels/js/components/Home/Home.jsx2
-rw-r--r--browser/components/pocket/content/panels/js/components/Saved/Saved.jsx2
-rw-r--r--browser/components/pocket/content/panels/js/home/overlay.jsx2
-rw-r--r--browser/components/pocket/content/panels/js/main.bundle.js14
-rw-r--r--browser/components/pocket/content/panels/js/main.mjs2
-rw-r--r--browser/components/pocket/content/panels/js/saved/overlay.jsx2
-rw-r--r--browser/components/pocket/content/panels/js/signup/overlay.jsx2
-rw-r--r--browser/components/pocket/content/panels/js/style-guide/overlay.jsx2
-rw-r--r--browser/components/pocket/content/pktApi.sys.mjs27
-rw-r--r--browser/components/pocket/content/pktUI.js28
-rw-r--r--browser/components/pocket/test/browser_pocket_button_icon_state.js10
-rw-r--r--browser/components/pocket/test/unit/browser_pocket_AboutPocketParent.js20
-rw-r--r--browser/components/preferences/dialogs/connection.js7
-rw-r--r--browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml1
-rw-r--r--browser/components/preferences/fxaPairDevice.xhtml1
-rw-r--r--browser/components/preferences/main.inc.xhtml14
-rw-r--r--browser/components/preferences/main.js43
-rw-r--r--browser/components/preferences/preferences.js35
-rw-r--r--browser/components/preferences/preferences.xhtml7
-rw-r--r--browser/components/preferences/privacy.inc.xhtml33
-rw-r--r--browser/components/preferences/privacy.js97
-rw-r--r--browser/components/preferences/sync.inc.xhtml39
-rw-r--r--browser/components/preferences/tests/browser.toml11
-rw-r--r--browser/components/preferences/tests/browser_applications_selection.js20
-rw-r--r--browser/components/preferences/tests/browser_bug731866.js9
-rw-r--r--browser/components/preferences/tests/browser_connection_system_wpad.js40
-rw-r--r--browser/components/preferences/tests/browser_contentblocking.js2
-rw-r--r--browser/components/preferences/tests/browser_keyboardfocus.js36
-rw-r--r--browser/components/preferences/tests/browser_primaryPassword.js3
-rw-r--r--browser/components/preferences/tests/browser_privacy_dnsoverhttps.js162
-rw-r--r--browser/components/preferences/tests/browser_search_quickactions.js26
-rw-r--r--browser/components/preferences/tests/browser_subdialogs.js15
-rw-r--r--browser/components/preferences/tests/siteData/browser.toml2
-rw-r--r--browser/components/preferences/tests/siteData/browser_clearSiteData.js41
-rw-r--r--browser/components/preferences/tests/siteData/browser_clearSiteData_v2.js258
-rw-r--r--browser/components/preferences/translations.inc.xhtml54
-rw-r--r--browser/components/preferences/translations.js270
-rw-r--r--browser/components/privatebrowsing/ResetPBMPanel.sys.mjs29
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_oa_private_browsing_window.js2
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_pin_promo.js54
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_dismiss.js2
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_nimbus_impressions.js4
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cache.js2
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_certexceptionsui.js2
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_cleanup.js2
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_favicon.js6
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_geoprompt_page.html2
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_last_private_browsing_context_exited.js4
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_lastpbcontextexited.js4
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_resetPBM.js10
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js2
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_ui.js2
-rw-r--r--browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js4
-rw-r--r--browser/components/protections/content/protections.html1
-rw-r--r--browser/components/protections/content/protections.mjs2
-rw-r--r--browser/components/protections/test/browser/browser_protections_monitor.js2
-rw-r--r--browser/components/protocolhandler/WebProtocolHandlerRegistrar.sys.mjs4
-rw-r--r--browser/components/reportbrokensite/ReportBrokenSite.sys.mjs2
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_back_buttons.js41
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_keyboard_navigation.js60
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_prefers_contrast.js2
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_reason_dropdown.js194
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_report_site_issue_fallback.js13
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_send_more_info.js29
-rw-r--r--browser/components/reportbrokensite/test/browser/browser_tab_key_order.js13
-rw-r--r--browser/components/reportbrokensite/test/browser/head.js2
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser.toml74
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvas_iframes.js217
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvas_popups.js198
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes.js263
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_aboutblank.js266
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_blob.js266
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_data.js271
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups.js268
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_aboutblank.js269
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_blob.js208
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_data.js208
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_dynamical_window_rounding.js2
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_etp_iframes.js2
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes.js19
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutblank.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutsrcdoc.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blob.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blobcrossorigin.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_data.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_sandboxediframe.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_aboutblank.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob_noopener.js26
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data.js15
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data_noopener.js26
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_noopener.js26
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_navigator.js2
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_spoofing_keyboard_event.js2
-rw-r--r--browser/components/resistfingerprinting/test/browser/browser_timezone.js9
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_animationapi_iframee.html2
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_animationapi_iframer.html2
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvas_iframee.html43
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvas_iframer.html55
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframee.html71
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframer.html31
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_popupmaker.html110
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframee.html75
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframer.html31
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_popupmaker.html92
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframee.html76
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframer.html31
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_data_popupmaker.html91
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_iframee.html43
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_canvascompare_iframer.html77
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_hwconcurrency_aboutblank_popupmaker.html2
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blob_popupmaker.html2
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_hwconcurrency_blobcrossorigin_iframer.html2
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_hwconcurrency_data_popupmaker.html6
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_hwconcurrency_iframer.html2
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_hwconcurrency_sandboxediframe_double_framee.html2
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_navigator_iframee.html2
-rw-r--r--browser/components/resistfingerprinting/test/browser/file_reduceTimePrecision_iframer.html2
-rw-r--r--browser/components/resistfingerprinting/test/browser/head.js81
-rw-r--r--browser/components/resistfingerprinting/test/mochitest/test_geolocation.html4
-rw-r--r--browser/components/safebrowsing/content/test/browser_whitelisted.js2
-rw-r--r--browser/components/safebrowsing/content/test/head.js2
-rw-r--r--browser/components/screenshots/ScreenshotsOverlayChild.sys.mjs624
-rw-r--r--browser/components/screenshots/ScreenshotsUtils.sys.mjs245
-rw-r--r--browser/components/screenshots/content/screenshots.css68
-rw-r--r--browser/components/screenshots/content/screenshots.html68
-rw-r--r--browser/components/screenshots/content/screenshots.js105
-rw-r--r--browser/components/screenshots/fileHelpers.mjs67
-rw-r--r--browser/components/screenshots/jar.mn6
-rw-r--r--browser/components/screenshots/overlay/overlay.css56
-rw-r--r--browser/components/screenshots/overlayHelpers.mjs63
-rw-r--r--browser/components/screenshots/screenshots-buttons.css9
-rw-r--r--browser/components/screenshots/screenshots-buttons.js44
-rw-r--r--browser/components/screenshots/screenshots-preview.css35
-rw-r--r--browser/components/screenshots/screenshots-preview.html25
-rw-r--r--browser/components/screenshots/screenshots-preview.mjs271
-rw-r--r--browser/components/screenshots/tests/browser/browser.toml16
-rw-r--r--browser/components/screenshots/tests/browser/browser_iframe_test.js4
-rw-r--r--browser/components/screenshots/tests/browser/browser_keyboard_shortcuts.js128
-rw-r--r--browser/components/screenshots/tests/browser/browser_keyboard_tests.js482
-rw-r--r--browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js3
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_download_filenames.js67
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_drag_scroll_test.js117
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_drag_test.js66
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js136
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js51
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js66
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js97
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_toggle_pref.js2
-rw-r--r--browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js105
-rw-r--r--browser/components/screenshots/tests/browser/browser_test_element_picker.js39
-rw-r--r--browser/components/screenshots/tests/browser/browser_test_resize.js4
-rw-r--r--browser/components/screenshots/tests/browser/browser_test_selection_size_text.js86
-rw-r--r--browser/components/screenshots/tests/browser/browser_text_selectionAPI_test.js55
-rw-r--r--browser/components/screenshots/tests/browser/head.js163
-rw-r--r--browser/components/screenshots/tests/browser/rtl-test-page.html8
-rw-r--r--browser/components/screenshots/tests/browser/test-selectionAPI-page.html10
-rw-r--r--browser/components/search/.eslintrc.js2
-rw-r--r--browser/components/search/DomainToCategoriesMap.worker.mjs101
-rw-r--r--browser/components/search/SearchSERPTelemetry.sys.mjs918
-rw-r--r--browser/components/search/content/autocomplete-popup.js3
-rw-r--r--browser/components/search/content/searchbar.js9
-rw-r--r--browser/components/search/metrics.yaml120
-rw-r--r--browser/components/search/moz.build6
-rw-r--r--browser/components/search/schema/search-telemetry-v2-schema.json (renamed from browser/components/search/schema/search-telemetry-schema.json)0
-rw-r--r--browser/components/search/schema/search-telemetry-v2-ui-schema.json (renamed from browser/components/search/schema/search-telemetry-ui-schema.json)2
-rw-r--r--browser/components/search/test/browser/telemetry/browser.toml29
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js17
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_enabled_by_nimbus_variable.js167
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_abandonment.js9
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component.js14
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_children.js1
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_parent.js1
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_timing.js3
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ad_values.js19
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js37
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_extraction.js46
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_no_sponsored_values.js144
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js302
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_region.js14
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js66
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js29
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js23
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached.js5
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached_serp.js3
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_content.js16
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_children.js1
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_parent.js1
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_ignoreLinkRegexps.js1
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_multiple_tabs.js5
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_nonAdsLinkQueryParamNames.js5
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_non_ad.js6
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_query_params.js10
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_redirect.js10
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_target.js14
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_new_window.js9
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_private.js7
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_remote_settings_sync.js5
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_shopping.js5
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_signed_in_to_account.js187
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_sources.js2
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_about.js3
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads.js12
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_clicks.js11
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_data_attributes.js9
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_load_events.js5
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_in_content.js15
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_navigation.js21
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_webextension.js3
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_in_content.js13
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_provider.js16
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_tab.js27
-rw-r--r--browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_single_tab.js18
-rw-r--r--browser/components/search/test/browser/telemetry/head.js59
-rw-r--r--browser/components/search/test/browser/telemetry/searchTelemetryDomainCategorizationReportingWithoutAds.html18
-rw-r--r--browser/components/search/test/browser/telemetry/searchTelemetryDomainExtraction.html31
-rw-r--r--browser/components/search/test/marionette/manifest.toml2
-rw-r--r--browser/components/search/test/marionette/telemetry/manifest.toml4
-rw-r--r--browser/components/search/test/marionette/telemetry/test_ping_submitted.py91
-rw-r--r--browser/components/search/test/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--browser/components/search/test/unit/test_domain_to_categories_store.js361
-rw-r--r--browser/components/search/test/unit/test_search_telemetry_categorization_sync.js75
-rw-r--r--browser/components/search/test/unit/test_search_telemetry_config_validation.js2
-rw-r--r--browser/components/search/test/unit/test_ui_schemas_valid.js31
-rw-r--r--browser/components/search/test/unit/test_urlTelemetry_generic.js32
-rw-r--r--browser/components/search/test/unit/xpcshell.toml11
-rw-r--r--browser/components/sessionstore/ContentRestore.sys.mjs435
-rw-r--r--browser/components/sessionstore/ContentSessionStore.sys.mjs685
-rw-r--r--browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs10
-rw-r--r--browser/components/sessionstore/SessionFile.sys.mjs88
-rw-r--r--browser/components/sessionstore/SessionLogger.sys.mjs87
-rw-r--r--browser/components/sessionstore/SessionSaver.sys.mjs2
-rw-r--r--browser/components/sessionstore/SessionStartup.sys.mjs97
-rw-r--r--browser/components/sessionstore/SessionStore.sys.mjs679
-rw-r--r--browser/components/sessionstore/SessionStoreFunctions.sys.mjs93
-rw-r--r--browser/components/sessionstore/StartupPerformance.sys.mjs2
-rw-r--r--browser/components/sessionstore/TabAttributes.sys.mjs43
-rw-r--r--browser/components/sessionstore/TabStateFlusher.sys.mjs100
-rw-r--r--browser/components/sessionstore/components.conf12
-rw-r--r--browser/components/sessionstore/content/aboutSessionRestore.js20
-rw-r--r--browser/components/sessionstore/content/content-sessionStore.js13
-rw-r--r--browser/components/sessionstore/jar.mn1
-rw-r--r--browser/components/sessionstore/moz.build10
-rw-r--r--browser/components/sessionstore/test/SessionStoreTestUtils.sys.mjs4
-rw-r--r--browser/components/sessionstore/test/browser.toml268
-rw-r--r--browser/components/sessionstore/test/browser_354894_perwindowpb.js10
-rw-r--r--browser/components/sessionstore/test/browser_394759_basic.js2
-rw-r--r--browser/components/sessionstore/test/browser_394759_behavior.js16
-rw-r--r--browser/components/sessionstore/test/browser_394759_purge.js2
-rw-r--r--browser/components/sessionstore/test/browser_459906.js4
-rw-r--r--browser/components/sessionstore/test/browser_461743.js2
-rw-r--r--browser/components/sessionstore/test/browser_464199.js2
-rw-r--r--browser/components/sessionstore/test/browser_464620_a.js2
-rw-r--r--browser/components/sessionstore/test/browser_464620_b.js2
-rw-r--r--browser/components/sessionstore/test/browser_526613.js2
-rw-r--r--browser/components/sessionstore/test/browser_580512.js6
-rw-r--r--browser/components/sessionstore/test/browser_586068-apptabs.js7
-rw-r--r--browser/components/sessionstore/test/browser_586068-browser_state_interrupted.js9
-rw-r--r--browser/components/sessionstore/test/browser_586068-multi_window.js9
-rw-r--r--browser/components/sessionstore/test/browser_586068-window_state.js7
-rw-r--r--browser/components/sessionstore/test/browser_586068-window_state_override.js7
-rw-r--r--browser/components/sessionstore/test/browser_589246.js12
-rw-r--r--browser/components/sessionstore/test/browser_590268.js2
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_duplicateTab.js4
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_setBrowserState.js2
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_setTabState.js6
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_setWindowState.js6
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseTab.js4
-rw-r--r--browser/components/sessionstore/test/browser_615394-SSWindowState_events_undoCloseWindow.js8
-rw-r--r--browser/components/sessionstore/test/browser_618151.js2
-rw-r--r--browser/components/sessionstore/test/browser_636279.js2
-rw-r--r--browser/components/sessionstore/test/browser_645428.js2
-rw-r--r--browser/components/sessionstore/test/browser_687710_2.js66
-rw-r--r--browser/components/sessionstore/test/browser_705597.js10
-rw-r--r--browser/components/sessionstore/test/browser_707862.js20
-rw-r--r--browser/components/sessionstore/test/browser_739531.js2
-rw-r--r--browser/components/sessionstore/test/browser_async_flushes.js52
-rw-r--r--browser/components/sessionstore/test/browser_async_remove_tab.js30
-rw-r--r--browser/components/sessionstore/test/browser_async_window_flushing.js11
-rw-r--r--browser/components/sessionstore/test/browser_attributes.js59
-rw-r--r--browser/components/sessionstore/test/browser_bfcache_telemetry.js3
-rw-r--r--browser/components/sessionstore/test/browser_closed_tabs_closed_windows.js10
-rw-r--r--browser/components/sessionstore/test/browser_cookies.js2
-rw-r--r--browser/components/sessionstore/test/browser_crashedTabs.js2
-rw-r--r--browser/components/sessionstore/test/browser_docshell_uuid_consistency.js94
-rw-r--r--browser/components/sessionstore/test/browser_frame_history.js2
-rw-r--r--browser/components/sessionstore/test/browser_frametree.js2
-rw-r--r--browser/components/sessionstore/test/browser_history_persist.js138
-rw-r--r--browser/components/sessionstore/test/browser_newtab_userTypedValue.js4
-rw-r--r--browser/components/sessionstore/test/browser_oldformat.toml301
-rw-r--r--browser/components/sessionstore/test/browser_parentProcessRestoreHash.js4
-rw-r--r--browser/components/sessionstore/test/browser_restoreLastClosedTabOrWindowOrSession.js2
-rw-r--r--browser/components/sessionstore/test/browser_send_async_message_oom.js75
-rw-r--r--browser/components/sessionstore/test/browser_sessionHistory.js9
-rw-r--r--browser/components/sessionstore/test/browser_sessionStoreContainer.js2
-rw-r--r--browser/components/sessionstore/test/browser_should_restore_tab.js4
-rw-r--r--browser/components/sessionstore/test/browser_windowStateContainer.js2
-rw-r--r--browser/components/sessionstore/test/head.js49
-rw-r--r--browser/components/sessionstore/test/marionette/manifest.toml4
-rw-r--r--browser/components/sessionstore/test/marionette/test_restore_sidebar.py110
-rw-r--r--browser/components/sessionstore/test/marionette/test_restore_sidebar_automatic.py110
-rw-r--r--browser/components/shell/HeadlessShell.sys.mjs2
-rw-r--r--browser/components/shell/ShellService.sys.mjs18
-rw-r--r--browser/components/shell/Windows11LimitedAccessFeatures.cpp281
-rw-r--r--browser/components/shell/Windows11LimitedAccessFeatures.h53
-rw-r--r--browser/components/shell/Windows11TaskbarPinning.cpp344
-rw-r--r--browser/components/shell/Windows11TaskbarPinning.h35
-rw-r--r--browser/components/shell/content/setDesktopBackground.js2
-rw-r--r--browser/components/shell/moz.build3
-rw-r--r--browser/components/shell/nsIWindowsShellService.idl12
-rw-r--r--browser/components/shell/nsWindowsShellService.cpp27
-rw-r--r--browser/components/shell/test/browser_1119088.js2
-rw-r--r--browser/components/shell/test/browser_420786.js2
-rw-r--r--browser/components/shell/test/browser_setDesktopBackgroundPreview.js2
-rw-r--r--browser/components/shell/test/head.js4
-rw-r--r--browser/components/shopping/content/shopping-message-bar.css56
-rw-r--r--browser/components/shopping/content/shopping-message-bar.mjs18
-rw-r--r--browser/components/shopping/content/shopping.html1
-rw-r--r--browser/components/shopping/metrics.yaml10
-rw-r--r--browser/components/shopping/tests/browser/browser_exposure_telemetry.js2
-rw-r--r--browser/components/shopping/tests/browser/browser_inprogress_analysis.js5
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_settings.js8
-rw-r--r--browser/components/shopping/tests/browser/browser_shopping_urlbar.js10
-rw-r--r--browser/components/shopping/tests/browser/browser_ui_telemetry.js2
-rw-r--r--browser/components/sidebar/browser-sidebar.js (renamed from browser/base/content/browser-sidebar.js)488
-rw-r--r--browser/components/sidebar/jar.mn12
-rw-r--r--browser/components/sidebar/moz.build2
-rw-r--r--browser/components/sidebar/sidebar-customize.css57
-rw-r--r--browser/components/sidebar/sidebar-customize.html31
-rw-r--r--browser/components/sidebar/sidebar-customize.mjs116
-rw-r--r--browser/components/sidebar/sidebar-history.html45
-rw-r--r--browser/components/sidebar/sidebar-history.mjs188
-rw-r--r--browser/components/sidebar/sidebar-main.css29
-rw-r--r--browser/components/sidebar/sidebar-main.mjs163
-rw-r--r--browser/components/sidebar/sidebar-page.mjs45
-rw-r--r--browser/components/sidebar/sidebar-syncedtabs.html44
-rw-r--r--browser/components/sidebar/sidebar-syncedtabs.mjs191
-rw-r--r--browser/components/sidebar/sidebar.css27
-rw-r--r--browser/components/sidebar/sidebar.ftl33
-rw-r--r--browser/components/sidebar/tests/browser/browser.toml7
-rw-r--r--browser/components/sidebar/tests/browser/browser_customize_sidebar.js64
-rw-r--r--browser/components/sidebar/tests/browser/browser_extensions_sidebar.js222
-rw-r--r--browser/components/sidebar/tests/browser/browser_history_sidebar.js97
-rw-r--r--browser/components/storybook/.storybook/addon-fluent/preset/manager.mjs2
-rw-r--r--browser/components/storybook/.storybook/main.js11
-rw-r--r--browser/components/storybook/.storybook/manager-head.html22
-rw-r--r--browser/components/storybook/.storybook/markdown-story-utils.js10
-rw-r--r--browser/components/storybook/.storybook/preview-head.html8
-rw-r--r--browser/components/storybook/.storybook/preview.mjs3
-rw-r--r--browser/components/storybook/docs/README.other-widgets.stories.md11
-rw-r--r--browser/components/storybook/docs/README.reusable-widgets.stories.md11
-rw-r--r--browser/components/storybook/docs/README.storybook.stories.md2
-rw-r--r--browser/components/storybook/stories/fxview-tab-list.stories.mjs3
-rw-r--r--browser/components/storybook/stories/shopping-message-bar.stories.mjs28
-rw-r--r--browser/components/syncedtabs/SyncedTabsDeckComponent.sys.mjs2
-rw-r--r--browser/components/syncedtabs/TabListView.sys.mjs7
-rw-r--r--browser/components/syncedtabs/sidebar.xhtml1
-rw-r--r--browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js25
-rw-r--r--browser/components/tabpreview/jar.mn2
-rw-r--r--browser/components/tabpreview/tab-preview-panel.mjs205
-rw-r--r--browser/components/tabpreview/tabpreview.css61
-rw-r--r--browser/components/tabpreview/tabpreview.mjs237
-rw-r--r--browser/components/tests/browser/browser.toml3
-rw-r--r--browser/components/tests/browser/browser_browserGlue_os_auth.js25
-rw-r--r--browser/components/tests/browser/browser_browserGlue_upgradeDialog_trigger.js80
-rw-r--r--browser/components/tests/browser/browser_contentpermissionprompt.js2
-rw-r--r--browser/components/tests/browser/browser_default_webprotocol_handler_mailto.js53
-rw-r--r--browser/components/tests/browser/browser_quit_disabled.js2
-rw-r--r--browser/components/tests/browser/head.js4
-rw-r--r--browser/components/tests/unit/test_browserGlue_migration_osauth.js159
-rw-r--r--browser/components/tests/unit/xpcshell.toml3
-rw-r--r--browser/components/topsites/TopSites.sys.mjs2039
-rw-r--r--browser/components/topsites/moz.build12
-rw-r--r--browser/components/topsites/test/unit/test_top_sites.js3571
-rw-r--r--browser/components/topsites/test/unit/xpcshell.toml4
-rw-r--r--browser/components/touchbar/MacTouchBar.sys.mjs4
-rw-r--r--browser/components/touchbar/tests/browser/browser_touchbar_tests.js2
-rw-r--r--browser/components/translations/content/TranslationsPanelShared.sys.mjs93
-rw-r--r--browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml8
-rw-r--r--browser/components/translations/content/fullPageTranslationsPanel.js97
-rw-r--r--browser/components/translations/content/selectTranslationsPanel.inc.xhtml239
-rw-r--r--browser/components/translations/content/selectTranslationsPanel.js1633
-rw-r--r--browser/components/translations/moz.build2
-rw-r--r--browser/components/translations/tests/browser/browser.toml60
-rw-r--r--browser/components/translations/tests/browser/browser_translations_about_preferences_manage_downloaded_languages.js8
-rw-r--r--browser/components/translations/tests/browser/browser_translations_about_preferences_settings_ui.js197
-rw-r--r--browser/components/translations/tests/browser/browser_translations_full_page_move_tab_to_new_window.js64
-rw-r--r--browser/components/translations/tests/browser/browser_translations_full_page_multiple_windows.js68
-rw-r--r--browser/components/translations/tests/browser/browser_translations_full_page_panel_init_failure.js25
-rw-r--r--browser/components/translations/tests/browser/browser_translations_full_page_panel_retry.js2
-rw-r--r--browser/components/translations/tests/browser/browser_translations_full_page_panel_switch_languages.js10
-rw-r--r--browser/components/translations/tests/browser/browser_translations_full_page_panel_unsupported_lang.js (renamed from browser/components/translations/tests/browser/browser_translations_full_page_panel_engine_unsupported_lang.js)3
-rw-r--r--browser/components/translations/tests/browser/browser_translations_full_page_telemetry_switch_languages.js16
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_engine_unsupported.js35
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_feature_disabled.js16
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_with_full_page_translations_active.js22
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_with_hyperlink.js20
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_with_no_text_selected.js6
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_context_menu_with_text_selected.js22
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_close_on_new_tab.js46
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_copy_button.js95
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_engine_cache.js59
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_fallback_to_doc_language.js38
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_init_failure.js119
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_language_selectors.js54
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_mainview_ui.js36
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_pdf.js42
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_reader_mode.js44
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_directly.js70
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js68
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_directly.js71
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_select_current_language_from_dropdown_menu.js71
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js66
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js66
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_settings_menu.js70
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_full_page_button.js83
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js67
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js67
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.js98
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_from_dropdown_menu.js99
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_open.js86
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_after_unsupported_language.js55
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_open.js92
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_retranslate.js128
-rw-r--r--browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js163
-rw-r--r--browser/components/translations/tests/browser/head.js1480
-rw-r--r--browser/components/uitour/UITour-lib.js2
-rw-r--r--browser/components/uitour/UITour.sys.mjs10
-rw-r--r--browser/components/uitour/test/browser_UITour.js2
-rw-r--r--browser/components/uitour/test/browser_UITour3.js10
-rw-r--r--browser/components/uitour/test/browser_UITour5.js2
-rw-r--r--browser/components/uitour/test/browser_UITour_defaultBrowser.js6
-rw-r--r--browser/components/uitour/test/browser_UITour_modalDialog.js2
-rw-r--r--browser/components/uitour/test/head.js13
-rw-r--r--browser/components/urlbar/.eslintrc.js2
-rw-r--r--browser/components/urlbar/ActionsProvider.sys.mjs110
-rw-r--r--browser/components/urlbar/ActionsProviderContextualSearch.sys.mjs121
-rw-r--r--browser/components/urlbar/ActionsProviderQuickActions.sys.mjs152
-rw-r--r--browser/components/urlbar/QuickActionsLoaderDefault.sys.mjs73
-rw-r--r--browser/components/urlbar/UrlbarController.sys.mjs128
-rw-r--r--browser/components/urlbar/UrlbarInput.sys.mjs193
-rw-r--r--browser/components/urlbar/UrlbarPrefs.sys.mjs35
-rw-r--r--browser/components/urlbar/UrlbarProviderAboutPages.sys.mjs4
-rw-r--r--browser/components/urlbar/UrlbarProviderAutofill.sys.mjs72
-rw-r--r--browser/components/urlbar/UrlbarProviderCalculator.sys.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderClipboard.sys.mjs5
-rw-r--r--browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs280
-rw-r--r--browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs24
-rw-r--r--browser/components/urlbar/UrlbarProviderInterventions.sys.mjs8
-rw-r--r--browser/components/urlbar/UrlbarProviderOmnibox.sys.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderPlaces.sys.mjs24
-rw-r--r--browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs357
-rw-r--r--browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs21
-rw-r--r--browser/components/urlbar/UrlbarProviderQuickSuggestContextualOptIn.sys.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderRecentSearches.sys.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderSearchSuggestions.sys.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderSearchTips.sys.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs4
-rw-r--r--browser/components/urlbar/UrlbarProviderTokenAliasEngines.sys.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderTopSites.sys.mjs10
-rw-r--r--browser/components/urlbar/UrlbarProviderUnitConversion.sys.mjs2
-rw-r--r--browser/components/urlbar/UrlbarProviderWeather.sys.mjs12
-rw-r--r--browser/components/urlbar/UrlbarProvidersManager.sys.mjs56
-rw-r--r--browser/components/urlbar/UrlbarTokenizer.sys.mjs17
-rw-r--r--browser/components/urlbar/UrlbarUtils.sys.mjs81
-rw-r--r--browser/components/urlbar/UrlbarValueFormatter.sys.mjs6
-rw-r--r--browser/components/urlbar/UrlbarView.sys.mjs66
-rw-r--r--browser/components/urlbar/docs/dynamic-result-types.rst6
-rw-r--r--browser/components/urlbar/docs/firefox-suggest-telemetry.rst8
-rw-r--r--browser/components/urlbar/metrics.yaml42
-rw-r--r--browser/components/urlbar/moz.build5
-rw-r--r--browser/components/urlbar/pings.yaml16
-rw-r--r--browser/components/urlbar/private/AddonSuggestions.sys.mjs10
-rw-r--r--browser/components/urlbar/private/AdmWikipedia.sys.mjs5
-rw-r--r--browser/components/urlbar/private/MDNSuggestions.sys.mjs10
-rw-r--r--browser/components/urlbar/private/SuggestBackendRust.sys.mjs14
-rw-r--r--browser/components/urlbar/private/YelpSuggestions.sys.mjs10
-rw-r--r--browser/components/urlbar/tests/UrlbarTestUtils.sys.mjs33
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_picks.js10
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_searchTips.js2
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js2
-rw-r--r--browser/components/urlbar/tests/browser/browser.toml23
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js2
-rw-r--r--browser/components/urlbar/tests/browser/browser_UrlbarInput_untrimOnUserInteraction.js124
-rw-r--r--browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js4
-rw-r--r--browser/components/urlbar/tests/browser/browser_acknowledgeFeedbackAndDismissal.js4
-rw-r--r--browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js1
-rw-r--r--browser/components/urlbar/tests/browser/browser_contextualsearch.js73
-rw-r--r--browser/components/urlbar/tests/browser/browser_copy_during_load.js2
-rw-r--r--browser/components/urlbar/tests/browser/browser_decode.js2
-rw-r--r--browser/components/urlbar/tests/browser/browser_dynamicResults.js4
-rw-r--r--browser/components/urlbar/tests/browser/browser_engagement.js29
-rw-r--r--browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js195
-rw-r--r--browser/components/urlbar/tests/browser/browser_less_common_selection_manipulations.js240
-rw-r--r--browser/components/urlbar/tests/browser/browser_locationBarCommand.js2
-rw-r--r--browser/components/urlbar/tests/browser/browser_oneOffs.js6
-rw-r--r--browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js2
-rw-r--r--browser/components/urlbar/tests/browser/browser_quickactions.js530
-rw-r--r--browser/components/urlbar/tests/browser/browser_quickactions_commands.js154
-rw-r--r--browser/components/urlbar/tests/browser/browser_quickactions_devtools.js23
-rw-r--r--browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js68
-rw-r--r--browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js10
-rw-r--r--browser/components/urlbar/tests/browser/browser_raceWithTabs.js4
-rw-r--r--browser/components/urlbar/tests/browser/browser_result_menu.js12
-rw-r--r--browser/components/urlbar/tests/browser/browser_secondaryActions.js141
-rw-r--r--browser/components/urlbar/tests/browser/browser_stop_pending.js4
-rw-r--r--browser/components/urlbar/tests/browser/browser_strip_on_share.js28
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js133
-rw-r--r--browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js4
-rw-r--r--browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js58
-rw-r--r--browser/components/urlbar/tests/browser/head.js71
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser.toml8
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js24
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js7
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_type.js8
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js5
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js26
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js2
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js12
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js75
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_tips.js2
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_type.js48
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_exposure.js2
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_potential_exposure.js438
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_reenter.js79
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js3
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js13
-rw-r--r--browser/components/urlbar/tests/engagementTelemetry/browser/head.js12
-rw-r--r--browser/components/urlbar/tests/quicksuggest/QuickSuggestTestUtils.sys.mjs17
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js73
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js7
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js5
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_mdn.js68
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_pocket.js20
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_yelp.js5
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js23
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_gleanEmptyStrings.js5
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js11
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js5
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js5
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js18
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/head.js41
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/head.js10
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js5
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_impressionCaps.js2
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js5
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merinoSessions.js2
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_scoreMap.js5
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js5
-rw-r--r--browser/components/urlbar/tests/quicksuggest/unit/test_weather.js2
-rw-r--r--browser/components/urlbar/tests/unit/test_autofill_origins.js65
-rw-r--r--browser/components/urlbar/tests/unit/test_exposure.js11
-rw-r--r--browser/components/urlbar/tests/unit/test_l10nCache.js2
-rw-r--r--browser/components/urlbar/tests/unit/test_quickactions.js109
-rw-r--r--browser/components/urlbar/tests/unit/test_tokenizer.js6
-rw-r--r--browser/config/mozconfigs/linux32/debug-fuzzing1
-rw-r--r--browser/config/mozconfigs/linux32/nightly-fuzzing-asan1
-rw-r--r--browser/config/mozconfigs/linux64/debug-asan2
-rw-r--r--browser/config/mozconfigs/linux64/debug-fuzzing1
-rw-r--r--browser/config/mozconfigs/linux64/debug-fuzzing-noopt1
-rw-r--r--browser/config/mozconfigs/linux64/fuzzing-ccov1
-rw-r--r--browser/config/mozconfigs/linux64/nightly-fuzzing-asan1
-rw-r--r--browser/config/mozconfigs/linux64/nightly-fuzzing-asan-afl9
-rw-r--r--browser/config/mozconfigs/linux64/nightly-fuzzing-asan-noopt1
-rw-r--r--browser/config/mozconfigs/linux64/nightly-fuzzing-asan-nyx1
-rw-r--r--browser/config/mozconfigs/linux64/tsan-fuzzing1
-rw-r--r--browser/config/mozconfigs/macosx64/debug-fuzzing1
-rw-r--r--browser/config/mozconfigs/macosx64/nightly-fuzzing-asan1
-rw-r--r--browser/config/mozconfigs/win32/debug-fuzzing1
-rw-r--r--browser/config/mozconfigs/win64/debug-fuzzing1
-rw-r--r--browser/config/mozconfigs/win64/fuzzing-ccov1
-rw-r--r--browser/config/mozconfigs/win64/nightly-fuzzing-asan1
-rw-r--r--browser/config/version.txt2
-rw-r--r--browser/config/version_display.txt2
-rw-r--r--browser/docs/index.rst2
-rw-r--r--browser/extensions/formautofill/api.js8
-rw-r--r--browser/extensions/formautofill/content/addressFormLayout.mjs187
-rw-r--r--browser/extensions/formautofill/content/autofillEditForms.js640
-rw-r--r--browser/extensions/formautofill/content/autofillEditForms.mjs288
-rw-r--r--browser/extensions/formautofill/content/customElements.js392
-rw-r--r--browser/extensions/formautofill/content/editAddress.xhtml94
-rw-r--r--browser/extensions/formautofill/content/editCreditCard.xhtml52
-rw-r--r--browser/extensions/formautofill/content/editDialog.mjs (renamed from browser/extensions/formautofill/content/editDialog.js)54
-rw-r--r--browser/extensions/formautofill/content/formautofill.css11
-rw-r--r--browser/extensions/formautofill/content/manageAddresses.xhtml11
-rw-r--r--browser/extensions/formautofill/content/manageCreditCards.xhtml11
-rw-r--r--browser/extensions/formautofill/content/manageDialog.mjs (renamed from browser/extensions/formautofill/content/manageDialog.js)80
-rw-r--r--browser/extensions/formautofill/locales/en-US/formautofill.properties18
-rw-r--r--browser/extensions/formautofill/moz.build3
-rw-r--r--browser/extensions/formautofill/skin/linux/autocomplete-item.css10
-rw-r--r--browser/extensions/formautofill/skin/osx/autocomplete-item.css18
-rw-r--r--browser/extensions/formautofill/skin/shared/autocomplete-item-shared.css182
-rw-r--r--browser/extensions/formautofill/skin/shared/editAddress.css15
-rw-r--r--browser/extensions/formautofill/skin/windows/autocomplete-item.css25
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser.toml2
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_state.js4
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui_lines.js32
-rw-r--r--browser/extensions/formautofill/test/browser/address/browser_edit_address_doorhanger_display_state.js5
-rw-r--r--browser/extensions/formautofill/test/browser/browser_autocomplete_footer.js29
-rw-r--r--browser/extensions/formautofill/test/browser/browser_dropdown_layout.js30
-rw-r--r--browser/extensions/formautofill/test/browser/browser_editAddressDialog.js153
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser.toml16
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js33
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js11
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js32
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js22
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js6
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js2
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_osAuth.js200
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js19
-rw-r--r--browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js14
-rw-r--r--browser/extensions/formautofill/test/browser/head.js71
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml1
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_basic_creditcard_autocomplete_form.html38
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html12
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html7
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_creditcard_autocomplete_off.html9
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html8
-rw-r--r--browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html8
-rw-r--r--browser/extensions/formautofill/test/mochitest/formautofill_common.js45
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_autofill_and_ordinal_forms.html20
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_autofocus_form.html8
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html30
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_form_changes.html21
-rw-r--r--browser/extensions/formautofill/test/mochitest/test_formautofill_preview_highlight.html2
-rw-r--r--browser/extensions/formautofill/test/unit/test_addressComponent_state.js12
-rw-r--r--browser/extensions/formautofill/test/unit/test_getRecords.js28
-rw-r--r--browser/extensions/formautofill/test/unit/test_phoneNumber.js2
-rw-r--r--browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js93
-rw-r--r--browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js154
-rw-r--r--browser/extensions/pictureinpicture/moz.build3
-rw-r--r--browser/extensions/pictureinpicture/video-wrappers/canalplus.js56
-rw-r--r--browser/extensions/pictureinpicture/video-wrappers/jwplayerWrapper.js (renamed from browser/extensions/pictureinpicture/video-wrappers/yahoo.js)9
-rw-r--r--browser/extensions/report-site-issue/experimentalAPIs/helpMenu.js2
-rw-r--r--browser/extensions/report-site-issue/test/browser/browser_report_site_issue.js6
-rw-r--r--browser/extensions/screenshots/background/main.js4
-rw-r--r--browser/extensions/screenshots/background/takeshot.js4
-rw-r--r--browser/extensions/screenshots/blobConverters.js4
-rw-r--r--browser/extensions/screenshots/build/thumbnailGenerator.js2
-rw-r--r--browser/extensions/screenshots/clipboard.js2
-rw-r--r--browser/extensions/screenshots/selector/ui.js6
-rw-r--r--browser/extensions/screenshots/selector/uicontrol.js4
-rw-r--r--browser/extensions/screenshots/sitehelper.js2
-rw-r--r--browser/extensions/webcompat/about-compat/aboutCompat.js2
-rw-r--r--browser/extensions/webcompat/data/shims.js4
-rw-r--r--browser/extensions/webcompat/data/ua_overrides.js80
-rw-r--r--browser/extensions/webcompat/experiment-apis/appConstants.js2
-rw-r--r--browser/extensions/webcompat/experiment-apis/systemManufacturer.js2
-rw-r--r--browser/extensions/webcompat/experiment-apis/trackingProtection.js4
-rw-r--r--browser/extensions/webcompat/injections/js/bug1769762-tiktok.com-plugins-shim.js2
-rw-r--r--browser/extensions/webcompat/injections/js/bug1855014-eksiseyler.com.js2
-rw-r--r--browser/extensions/webcompat/lib/custom_functions.js2
-rw-r--r--browser/extensions/webcompat/lib/shims.js2
-rw-r--r--browser/extensions/webcompat/lib/ua_overrides.js2
-rw-r--r--browser/extensions/webcompat/manifest.json2
-rw-r--r--browser/extensions/webcompat/shims/eluminate.js2
-rw-r--r--browser/extensions/webcompat/shims/google-ima.js14
-rw-r--r--browser/extensions/webcompat/shims/rambler-authenticator.js2
-rw-r--r--browser/extensions/webcompat/shims/webtrends.js2
-rw-r--r--browser/fxr/content/fxrui.js6
-rw-r--r--browser/installer/package-manifest.in18
-rw-r--r--browser/installer/removed-files.in12
-rw-r--r--browser/installer/windows/docs/FullInstaller.rst2
-rw-r--r--browser/installer/windows/docs/NSISPlugins.rst101
-rw-r--r--browser/installer/windows/docs/newProjectDllVS.pngbin0 -> 27399 bytes
-rw-r--r--browser/installer/windows/docs/projectPropertyPageVS.pngbin0 -> 258168 bytes
-rw-r--r--browser/installer/windows/docs/projectSettingsDllVS.pngbin0 -> 144260 bytes
-rw-r--r--browser/installer/windows/msix/AppxManifest.xml.in12
-rw-r--r--browser/installer/windows/nsis/defines.nsi.in4
-rwxr-xr-xbrowser/installer/windows/nsis/installer.nsi19
-rw-r--r--browser/installer/windows/nsis/maintenanceservice_installer.nsi2
-rwxr-xr-xbrowser/installer/windows/nsis/shared.nsh8
-rw-r--r--browser/locales-preview/backupSettings.ftl5
-rw-r--r--browser/locales-preview/select-translations.ftl51
-rw-r--r--browser/locales-preview/translations.ftl3
-rw-r--r--browser/locales/en-US/browser/aboutLogins.ftl8
-rw-r--r--browser/locales/en-US/browser/accounts.ftl2
-rw-r--r--browser/locales/en-US/browser/allTabsMenu.ftl3
-rw-r--r--browser/locales/en-US/browser/appmenu.ftl16
-rw-r--r--browser/locales/en-US/browser/browser.ftl16
-rw-r--r--browser/locales/en-US/browser/confirmationHints.ftl7
-rw-r--r--browser/locales/en-US/browser/defaultBrowserNotification.ftl17
-rw-r--r--browser/locales/en-US/browser/menubar.ftl2
-rw-r--r--browser/locales/en-US/browser/newtab/asrouter.ftl2
-rw-r--r--browser/locales/en-US/browser/newtab/newtab.ftl54
-rw-r--r--browser/locales/en-US/browser/newtab/onboarding.ftl4
-rw-r--r--browser/locales/en-US/browser/policies/policies-descriptions.ftl15
-rw-r--r--browser/locales/en-US/browser/preferences/preferences.ftl36
-rw-r--r--browser/locales/en-US/browser/reportBrokenSite.ftl2
-rw-r--r--browser/locales/en-US/browser/screenshots.ftl41
-rw-r--r--browser/locales/en-US/browser/screenshotsOverlay.ftl15
-rw-r--r--browser/locales/en-US/browser/sidebarMenu.ftl3
-rw-r--r--browser/locales/en-US/browser/tabContextMenu.ftl3
-rw-r--r--browser/locales/en-US/browser/tabbrowser.ftl5
-rw-r--r--browser/locales/en-US/browser/translations.ftl109
-rw-r--r--browser/locales/en-US/browser/webProtocolHandler.ftl11
-rw-r--r--browser/locales/en-US/chrome/browser/browser.properties7
-rw-r--r--browser/locales/en-US/crashreporter/crashreporter-override.ini9
-rw-r--r--browser/locales/jar.mn3
-rw-r--r--browser/locales/l10n-changesets.json305
-rw-r--r--browser/locales/l10n-onchange-changesets.json5
-rw-r--r--browser/locales/moz.build3
-rw-r--r--browser/modules/AboutNewTab.sys.mjs2
-rw-r--r--browser/modules/AsyncTabSwitcher.sys.mjs2
-rw-r--r--browser/modules/BackgroundTask_install.sys.mjs2
-rw-r--r--browser/modules/BackgroundTask_uninstall.sys.mjs2
-rw-r--r--browser/modules/BrowserUsageTelemetry.sys.mjs37
-rw-r--r--browser/modules/BrowserWindowTracker.sys.mjs2
-rw-r--r--browser/modules/ContentCrashHandlers.sys.mjs4
-rw-r--r--browser/modules/EveryWindow.sys.mjs2
-rw-r--r--browser/modules/ExtensionsUI.sys.mjs14
-rw-r--r--browser/modules/FaviconLoader.sys.mjs2
-rw-r--r--browser/modules/FirefoxBridgeExtensionUtils.sys.mjs121
-rw-r--r--browser/modules/HomePage.sys.mjs2
-rw-r--r--browser/modules/PageActions.sys.mjs2
-rw-r--r--browser/modules/PermissionUI.sys.mjs8
-rw-r--r--browser/modules/ProcessHangMonitor.sys.mjs9
-rw-r--r--browser/modules/Sanitizer.sys.mjs28
-rw-r--r--browser/modules/SiteDataManager.sys.mjs45
-rw-r--r--browser/modules/SitePermissions.sys.mjs2
-rw-r--r--browser/modules/TabUnloader.sys.mjs2
-rw-r--r--browser/modules/TabsList.sys.mjs2
-rw-r--r--browser/modules/WindowsJumpLists.sys.mjs6
-rw-r--r--browser/modules/WindowsPreviewPerTab.sys.mjs6
-rw-r--r--browser/modules/ZoomUI.sys.mjs4
-rw-r--r--browser/modules/metrics.yaml46
-rw-r--r--browser/modules/pings.yaml22
-rw-r--r--browser/modules/test/browser/browser.toml4
-rw-r--r--browser/modules/test/browser/browser_PageActions.js6
-rw-r--r--browser/modules/test/browser/browser_ProcessHangNotifications.js9
-rw-r--r--browser/modules/test/browser/browser_UnsubmittedCrashHandler.js8
-rw-r--r--browser/modules/test/browser/browser_UsageTelemetry_interaction.js222
-rw-r--r--browser/modules/test/browser/browser_UsageTelemetry_private_and_restore.js2
-rw-r--r--browser/modules/test/browser/browser_preloading_tab_moving.js2
-rw-r--r--browser/modules/test/browser/formValidation/browser_form_validation.js8
-rw-r--r--browser/modules/test/browser/head.js2
-rw-r--r--browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js463
-rw-r--r--browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js115
-rw-r--r--browser/modules/webrtcUI.sys.mjs4
-rw-r--r--browser/moz.build2
-rw-r--r--browser/themes/BuiltInThemes.sys.mjs4
-rw-r--r--browser/themes/ThemeVariableMap.sys.mjs12
-rw-r--r--browser/themes/linux/browser.css6
-rw-r--r--browser/themes/osx/browser.css50
-rw-r--r--browser/themes/osx/customizableui/panelUI.css4
-rw-r--r--browser/themes/osx/places/organizer.css160
-rw-r--r--browser/themes/shared/addons/unified-extensions.css96
-rw-r--r--browser/themes/shared/autocomplete.css34
-rw-r--r--browser/themes/shared/browser-custom-colors.css6
-rw-r--r--browser/themes/shared/browser-shared.css71
-rw-r--r--browser/themes/shared/controlcenter/dashboard.svg2
-rw-r--r--browser/themes/shared/controlcenter/panel.css6
-rw-r--r--browser/themes/shared/customizableui/customizeMode.css24
-rw-r--r--browser/themes/shared/customizableui/panelUI-shared.css179
-rw-r--r--browser/themes/shared/downloads/progressmeter.css13
-rw-r--r--browser/themes/shared/formautofill-notification.css2
-rw-r--r--browser/themes/shared/formautofill/icon-address-edit.svg6
-rw-r--r--browser/themes/shared/icons/back.svg2
-rw-r--r--browser/themes/shared/icons/circle-check-dotted.svg5
-rw-r--r--browser/themes/shared/icons/forward.svg2
-rw-r--r--browser/themes/shared/icons/ion.svg2
-rw-r--r--browser/themes/shared/jar.inc.mn1
-rw-r--r--browser/themes/shared/notification-icons.css185
-rw-r--r--browser/themes/shared/pageInfo.css15
-rw-r--r--browser/themes/shared/preferences/preferences.css23
-rw-r--r--browser/themes/shared/preferences/privacy.css8
-rw-r--r--browser/themes/shared/preferences/translations.css62
-rw-r--r--browser/themes/shared/sanitizeDialog_v2.css8
-rw-r--r--browser/themes/shared/tabs.css217
-rw-r--r--browser/themes/shared/toolbarbutton-icons.css31
-rw-r--r--browser/themes/shared/toolbarbuttons.css2
-rw-r--r--browser/themes/shared/translations/panel.css74
-rw-r--r--browser/themes/shared/urlbar-searchbar.css42
-rw-r--r--browser/themes/shared/urlbarView.css80
-rw-r--r--browser/themes/triage.json104
-rw-r--r--browser/themes/windows/browser.css20
-rw-r--r--browser/themes/windows/places/organizer.css1
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/Screenshot.sys.mjs4
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/AppMenu.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Buttons.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/ControlCenter.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/CustomizeMode.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/DevTools.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/LightweightThemes.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/PermissionPrompts.sys.mjs12
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Preferences.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.sys.mjs8
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/TabsInTitlebar.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Toolbars.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/UIDensities.sys.mjs2
-rw-r--r--browser/tools/mozscreenshots/mozscreenshots/extension/configurations/WindowSize.sys.mjs8
-rw-r--r--browser/tools/mozscreenshots/permissionPrompts/browser.toml1
1600 files changed, 59142 insertions, 22765 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/ClickHandlerParent.sys.mjs b/browser/actors/ClickHandlerParent.sys.mjs
index 4078c6404f..bdb722d958 100644
--- a/browser/actors/ClickHandlerParent.sys.mjs
+++ b/browser/actors/ClickHandlerParent.sys.mjs
@@ -6,6 +6,7 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
@@ -99,7 +100,7 @@ export class ClickHandlerParent extends JSWindowActorParent {
}
// This part is based on handleLinkClick.
- var where = window.whereToOpenLink(data);
+ var where = lazy.BrowserUtils.whereToOpenLink(data);
if (where == "current") {
return;
}
diff --git a/browser/actors/ContentSearchParent.sys.mjs b/browser/actors/ContentSearchParent.sys.mjs
index 73b881881b..3b27eabd14 100644
--- a/browser/actors/ContentSearchParent.sys.mjs
+++ b/browser/actors/ContentSearchParent.sys.mjs
@@ -6,6 +6,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SearchSuggestionController:
@@ -216,7 +217,7 @@ export let ContentSearch = {
// message and the time we handle it.
return;
}
- let where = win.whereToOpenLink(data.originalEvent);
+ let where = lazy.BrowserUtils.whereToOpenLink(data.originalEvent);
// There is a chance that by the time we receive the search message, the user
// has switched away from the tab that triggered the search. If, based on the
@@ -510,10 +511,11 @@ export let ContentSearch = {
lazy.UrlbarPrefs.get("shouldHandOffToSearchMode")
);
break;
- default:
+ default: {
let state = await this.currentStateObj();
this._broadcast("CurrentState", state);
break;
+ }
}
},
diff --git a/browser/actors/ContextMenuChild.sys.mjs b/browser/actors/ContextMenuChild.sys.mjs
index e16efdc9cd..2b98bea65e 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 (
@@ -535,6 +535,23 @@ export class ContextMenuChild extends JSWindowActorChild {
}
let doc = aEvent.composedTarget.ownerDocument;
+ if (!doc && Cu.isInAutomation) {
+ // doc has been observed to be null for many years, causing intermittent
+ // test failures all over the place (bug 1478596). The rate of failures
+ // is too low to debug locally, but frequent enough to be a nuisance.
+ // TODO bug 1478596: use these diagnostic logs to resolve the bug.
+ dump(
+ `doc is unexpectedly null (bug 1478596), composedTarget=${aEvent.composedTarget}\n`
+ );
+ // A potential fix is to fall back to aEvent.target.ownerDocument, per
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1478596#c1
+ // Let's print potentially viable alternatives to see what we should use.
+ for (let k of ["target", "originalTarget", "explicitOriginalTarget"]) {
+ dump(
+ ` Alternative: ${k}=${aEvent[k]} and its doc=${aEvent[k]?.ownerDocument}\n`
+ );
+ }
+ }
let {
mozDocumentURIIfNotForErrorPages: docLocation,
characterSet: charSet,
@@ -696,7 +713,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/ScreenshotsComponentChild.sys.mjs b/browser/actors/ScreenshotsComponentChild.sys.mjs
index 06d7204803..b578dfe7fa 100644
--- a/browser/actors/ScreenshotsComponentChild.sys.mjs
+++ b/browser/actors/ScreenshotsComponentChild.sys.mjs
@@ -48,6 +48,11 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
return this.removeEventListeners();
case "Screenshots:AddEventListeners":
return this.addEventListeners();
+ case "Screenshots:MoveFocusToContent":
+ return this.focusOverlay();
+ case "Screenshots:ClearFocus":
+ Services.focus.clearFocus(this.contentWindow);
+ return null;
}
return null;
}
@@ -64,6 +69,7 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
case "pointerup":
case "keyup":
case "keydown":
+ case "selectionchange":
if (!this.overlay?.initialized) {
return;
}
@@ -98,8 +104,8 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
this.requestDownloadScreenshot(event.detail.region);
break;
case "Screenshots:OverlaySelection": {
- let { hasSelection } = event.detail;
- this.sendOverlaySelection({ hasSelection });
+ let { hasSelection, overlayState } = event.detail;
+ this.sendOverlaySelection({ hasSelection, overlayState });
break;
}
case "Screenshots:RecordEvent": {
@@ -108,10 +114,13 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
break;
}
case "Screenshots:ShowPanel":
- this.showPanel();
+ this.sendAsyncMessage("Screenshots:ShowPanel");
break;
case "Screenshots:HidePanel":
- this.hidePanel();
+ this.sendAsyncMessage("Screenshots:HidePanel");
+ break;
+ case "Screenshots:FocusPanel":
+ this.sendAsyncMessage("Screenshots:MoveFocusToParent", event.detail);
break;
}
}
@@ -150,14 +159,6 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
this.endScreenshotsOverlay({ doNotResetMethods: true });
}
- showPanel() {
- this.sendAsyncMessage("Screenshots:ShowPanel");
- }
-
- hidePanel() {
- this.sendAsyncMessage("Screenshots:HidePanel");
- }
-
getDocumentTitle() {
return this.document.title;
}
@@ -172,6 +173,11 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
return methodsUsed;
}
+ focusOverlay() {
+ this.contentWindow.focus();
+ this.#overlay.focus();
+ }
+
/**
* Resolves when the document is ready to have an overlay injected into it.
*
@@ -220,6 +226,7 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
chromeEventHandler.addEventListener(event, this, true);
}
+ this.document.addEventListener("selectionchange", this);
}
/**
@@ -257,6 +264,7 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
for (let event of ScreenshotsComponentChild.OVERLAY_EVENTS) {
chromeEventHandler.removeEventListener(event, this, true);
}
+ this.document.removeEventListener("selectionchange", this);
}
/**
@@ -308,8 +316,8 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
let rect = {
left: scrollMinX,
top: scrollMinY,
- right: scrollWidth,
- bottom: scrollHeight,
+ right: scrollMinX + scrollWidth,
+ bottom: scrollMinY + scrollHeight,
width: scrollWidth,
height: scrollHeight,
devicePixelRatio,
@@ -341,13 +349,18 @@ export class ScreenshotsComponentChild extends JSWindowActorChild {
* The height of the content window.
*/
getVisibleBounds() {
- let { scrollX, scrollY, clientWidth, clientHeight, devicePixelRatio } =
- this.#overlay.windowDimensions.dimensions;
+ let {
+ pageScrollX,
+ pageScrollY,
+ clientWidth,
+ clientHeight,
+ devicePixelRatio,
+ } = this.#overlay.windowDimensions.dimensions;
let rect = {
- left: scrollX,
- top: scrollY,
- right: scrollX + clientWidth,
- bottom: scrollY + clientHeight,
+ left: pageScrollX,
+ top: pageScrollY,
+ right: pageScrollX + clientWidth,
+ bottom: pageScrollY + clientHeight,
width: clientWidth,
height: clientHeight,
devicePixelRatio,
diff --git a/browser/actors/SearchSERPTelemetryChild.sys.mjs b/browser/actors/SearchSERPTelemetryChild.sys.mjs
index c760f9a19e..b2b78941ad 100644
--- a/browser/actors/SearchSERPTelemetryChild.sys.mjs
+++ b/browser/actors/SearchSERPTelemetryChild.sys.mjs
@@ -13,13 +13,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
- "serpEventsEnabled",
- "browser.search.serpEventTelemetry.enabled",
- true
-);
-
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
"serpEventTelemetryCategorization",
"browser.search.serpEventTelemetryCategorization.enabled",
false
@@ -1077,7 +1070,11 @@ class DomainExtractor {
break;
}
case "textContent": {
- this.#fromElementsRetrieveTextContent(elements, extractedDomains);
+ this.#fromElementsRetrieveTextContent(
+ elements,
+ extractedDomains,
+ providerName
+ );
break;
}
}
@@ -1197,8 +1194,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 +1224,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);
}
}
}
@@ -1368,7 +1389,6 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
}
if (
- lazy.serpEventsEnabled &&
providerInfo.components?.length &&
(eventType == "load" || eventType == "pageshow")
) {
@@ -1496,17 +1516,13 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
// so that we remain consistent with the *.in-content:sap* count for the
// SEARCH_COUNTS histogram.
if (event.persisted) {
+ this.#checkForPageImpressionComponents();
this.#check(event.type);
- if (lazy.serpEventsEnabled) {
- this.#checkForPageImpressionComponents();
- }
}
break;
}
case "DOMContentLoaded": {
- if (lazy.serpEventsEnabled) {
- this.#checkForPageImpressionComponents();
- }
+ this.#checkForPageImpressionComponents();
this.#check(event.type);
break;
}
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..73b400d58f 100644
--- a/browser/app/macbuild/Contents/Info.plist.in
+++ b/browser/app/macbuild/Contents/Info.plist.in
@@ -232,22 +232,6 @@
<string>file</string>
</array>
</dict>
- <dict>
- <key>CFBundleURLName</key>
- <string>Firefox Protocol</string>
- <key>CFBundleURLSchemes</key>
- <array>
- <string>firefox-bridge</string>
- </array>
- </dict>
- <dict>
- <key>CFBundleURLName</key>
- <string>Firefox Private Browsing Protocol</string>
- <key>CFBundleURLSchemes</key>
- <array>
- <string>firefox-private-bridge</string>
- </array>
- </dict>
</array>
<key>CFBundleVersion</key>
<string>@MAC_BUNDLE_VERSION@</string>
@@ -291,5 +275,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/moz.build b/browser/app/moz.build
index c731e9798a..434167c996 100644
--- a/browser/app/moz.build
+++ b/browser/app/moz.build
@@ -62,6 +62,8 @@ if CONFIG["LIBFUZZER"]:
LOCAL_INCLUDES += [
"/tools/fuzzing/libfuzzer",
]
+elif CONFIG["FUZZING_INTERFACES"]:
+ USE_LIBS += ["fuzzer-interface"]
if CONFIG["MOZ_GECKODRIVER"]:
DEFINES["MOZ_GECKODRIVER"] = True
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/nsBrowserApp.cpp b/browser/app/nsBrowserApp.cpp
index 3145342155..e1f11b9cfd 100644
--- a/browser/app/nsBrowserApp.cpp
+++ b/browser/app/nsBrowserApp.cpp
@@ -192,6 +192,9 @@ static int do_main(int argc, char* argv[], char* envp[]) {
#ifdef LIBFUZZER
shellData.fuzzerDriver = fuzzer::FuzzerDriver;
#endif
+#ifdef AFLFUZZ
+ shellData.fuzzerDriver = afl_interface_raw;
+#endif
return gBootstrap->XRE_XPCShellMain(--argc, argv, envp, &shellData);
}
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 32cd57b0ed..27c2d13fbd 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -23,8 +23,6 @@ pref("browser.hiddenWindowChromeURL", "chrome://browser/content/hiddenWindowMac.
// Set add-ons abuse report related prefs specific to Firefox Desktop.
pref("extensions.abuseReport.enabled", true);
-pref("extensions.abuseReport.amWebAPI.enabled", true);
-pref("extensions.abuseReport.amoFormEnabled", true);
// Enables some extra Extension System Logging (can reduce performance)
pref("extensions.logging.enabled", false);
@@ -326,7 +324,7 @@ pref("browser.startup.couldRestoreSession.count", 0);
pref("browser.startup.preXulSkeletonUI", true);
// Whether the checkbox to enable Windows launch on login is shown
-pref("browser.startup.windowsLaunchOnLogin.enabled", false);
+pref("browser.startup.windowsLaunchOnLogin.enabled", true);
// Whether to show the launch on login infobar notification
pref("browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt", false);
#endif
@@ -356,6 +354,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 +397,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
@@ -413,13 +420,7 @@ pref("browser.urlbar.suggest.engines", true);
pref("browser.urlbar.suggest.calculator", false);
pref("browser.urlbar.suggest.recentsearches", true);
-#if defined(EARLY_BETA_OR_EARLIER)
- // Enable QuickActions and its urlbar search mode button.
- pref("browser.urlbar.quickactions.enabled", true);
- pref("browser.urlbar.suggest.quickactions", true);
- pref("browser.urlbar.shortcuts.quickactions", true);
- pref("browser.urlbar.quickactions.showPrefs", true);
-#endif
+pref("browser.urlbar.secondaryActions.featureGate", false);
#if defined(EARLY_BETA_OR_EARLIER)
// Enable Trending suggestions.
@@ -434,7 +435,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.
@@ -532,8 +533,10 @@ pref("browser.urlbar.trimURLs", true);
#ifdef NIGHTLY_BUILD
pref("browser.urlbar.trimHttps", true);
+pref("browser.urlbar.untrimOnUserInteraction.featureGate", true);
#else
pref("browser.urlbar.trimHttps", false);
+pref("browser.urlbar.untrimOnUserInteraction.featureGate", false);
#endif
// If changed to true, copying the entire URL from the location bar will put the
@@ -718,13 +721,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/");
@@ -743,11 +739,12 @@ pref("browser.search.separatePrivateDefault.ui.enabled", false);
// The maximum amount of times the private default banner is shown.
pref("browser.search.separatePrivateDefault.ui.banner.max", 0);
-// Enables search SERP telemetry (impressions, engagements and abandonment)
-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,16 +808,16 @@ 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");
// Whether the user has opted-in to recommended settings for data features.
pref("browser.dataFeatureRecommendations.enabled", false);
-// Temporary pref to control whether or not Private Browsing windows show up
-// as separate icons in the Windows taskbar.
-pref("browser.privateWindowSeparation.enabled", true);
-
// Use dark theme variant for PBM windows. This is only supported if the theme
// sets darkTheme data.
pref("browser.theme.dark-private-windows", true);
@@ -944,11 +941,7 @@ pref("browser.tabs.tooltipsShowPidAndActiveness", false);
pref("browser.tabs.cardPreview.enabled", false);
pref("browser.tabs.cardPreview.showThumbnails", true);
-pref("browser.tabs.firefox-view", true);
-pref("browser.tabs.firefox-view-next", true);
-pref("browser.tabs.firefox-view-newIcon", true);
pref("browser.tabs.firefox-view.logLevel", "Warn");
-pref("browser.tabs.firefox-view.notify-for-tabs", false);
// allow_eval_* is enabled on Firefox Desktop only at this
// point in time
@@ -1094,11 +1087,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
@@ -1272,14 +1261,33 @@ pref("browser.sessionstore.interval.idle", 3600000); // 1h
// collect/save the session quite as often.
pref("browser.sessionstore.idleDelay", 180); // 3 minutes
+// Fine-grained default logging levels for each log appender
+pref("browser.sessionstore.log.appender.console", "Fatal");
+pref("browser.sessionstore.log.appender.dump", "Error");
+pref("browser.sessionstore.log.appender.file.level", "Trace");
+pref("browser.sessionstore.log.appender.file.logOnError", true);
+
+// The default log level for all Session restore logs.
+pref("browser.sessionstore.loglevel", "Warn");
+
+#ifdef EARLY_BETA_OR_EARLIER
+ pref("browser.sessionstore.loglevel", "Debug");
+ pref("browser.sessionstore.log.appender.file.logOnSuccess", true);
+#else
+ pref("browser.sessionstore.log.appender.file.logOnSuccess", false);
+#endif
+// How old can a log file be before it gets deleted?
+pref("browser.sessionstore.log.appender.file.maxErrorAge", 864000); // 10 days
+
// on which sites to save text data, POSTDATA and cookies
// 0 = everywhere, 1 = unencrypted sites, 2 = nowhere
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);
@@ -1303,7 +1311,7 @@ pref("browser.sessionstore.restore_pinned_tabs_on_demand", false);
pref("browser.sessionstore.upgradeBackup.latestBuildID", "");
// How many upgrade backups should be kept
pref("browser.sessionstore.upgradeBackup.maxUpgradeBackups", 3);
-// End-users should not run sessionstore in debug mode
+// Toggle some debug behavior; end-users should not run sessionstore in debug mode
pref("browser.sessionstore.debug", false);
// Forget closed windows/tabs after two weeks
pref("browser.sessionstore.cleanup.forget_closed_after", 1209600000);
@@ -1422,7 +1430,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,21 +1690,24 @@ 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);
+// Activates preloading of the new tab url.
+pref("browser.newtab.preload", 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);
+// Weather widget for newtab
+pref("browser.newtabpage.activity-stream.showWeather", true);
+pref("browser.newtabpage.activity-stream.weather.query", "");
+pref("browser.newtabpage.activity-stream.weather.locationSearchEnabled", false);
+pref("browser.newtabpage.activity-stream.weather.temperatureUnits", "f");
+pref("browser.newtabpage.activity-stream.weather.display", "simple");
+// List of regions that get weather by default.
+pref("browser.newtabpage.activity-stream.discoverystream.region-weather-config", "");
-// 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);
+// Preference to enable wallpaper selection in the Customize Menu of new tab page
+pref("browser.newtabpage.activity-stream.newtabWallpapers.enabled", false);
-// Activates preloading of the new tab url.
-pref("browser.newtab.preload", true);
+// 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");
@@ -1709,7 +1724,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}");
@@ -1747,7 +1761,7 @@ pref("browser.newtabpage.activity-stream.discoverystream.spoc-positions", "1,5,7
pref("browser.newtabpage.activity-stream.discoverystream.spoc-topsites-positions", "2");
// This is a 0-based index, for consistency with the other position CSVs,
// but Contile positions are a 1-based index, so we end up adding 1 to these before using them.
-pref("browser.newtabpage.activity-stream.discoverystream.contile-topsites-positions", "0,1");
+pref("browser.newtabpage.activity-stream.discoverystream.contile-topsites-positions", "0,1,2");
pref("browser.newtabpage.activity-stream.discoverystream.widget-positions", "");
pref("browser.newtabpage.activity-stream.discoverystream.spocs-endpoint", "");
@@ -1784,6 +1798,9 @@ pref("browser.newtabpage.activity-stream.discoverystream.region-spocs-config", "
// List of regions that don't get the 7 row layout.
pref("browser.newtabpage.activity-stream.discoverystream.region-basic-config", "");
+// Add parameters to Pocket feed URL.
+pref("browser.newtabpage.activity-stream.discoverystream.pocket-feed-parameters", "");
+
// Allows Pocket story collections to be dismissed.
pref("browser.newtabpage.activity-stream.discoverystream.isCollectionDismissible", true);
pref("browser.newtabpage.activity-stream.discoverystream.personalization.enabled", true);
@@ -1821,9 +1838,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 +1869,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);
@@ -1939,6 +1952,10 @@ pref("identity.mobilepromo.ios", "https://www.mozilla.org/firefox/ios/?utm_sourc
// Default is 24 hours.
pref("identity.fxaccounts.commands.missed.fetch_interval", 86400);
+// Controls whether this client can send and receive "close tab"
+// commands from other FxA clients
+pref("identity.fxaccounts.commands.remoteTabManagement.enabled", false);
+
// Note: when media.gmp-*.visible is true, provided we're running on a
// supported platform/OS version, the corresponding CDM appears in the
// plugins list, Firefox will download the GMP/CDM if enabled, and our
@@ -1986,7 +2003,13 @@ pref("browser.translations.newSettingsUI.enable", false);
// Enable Firefox Select translations powered by Bergamot translations
// engine https://browser.mt/.
-pref("browser.translations.select.enable", false);
+#if defined(EARLY_BETA_OR_EARLIER)
+ // Enables Select Translations for Early Beta and Nightly.
+ pref("browser.translations.select.enable", true);
+#else
+ // Disables Select Translations for Late Beta and Release.
+ pref("browser.translations.select.enable", false);
+#endif
// Telemetry settings.
// Determines if Telemetry pings can be archived locally.
@@ -2233,6 +2256,9 @@ pref("privacy.exposeContentTitleInWindow.pbm", true);
// Run media transport in a separate process?
pref("media.peerconnection.mtransport_process", true);
+// Whether the "Close duplicate tabs" tab context menu is enabled.
+pref("browser.tabs.context.close-duplicate.enabled", true);
+
// For speculatively warming up tabs to improve perceived
// performance while using the async tab switcher.
pref("browser.tabs.remote.warmup.enabled", true);
@@ -2347,11 +2373,6 @@ pref("extensions.pocket.refresh.hideRecentSaves.enabled", false);
pref("signon.management.page.fileImport.enabled", true);
-#ifdef NIGHTLY_BUILD
-pref("signon.management.page.os-auth.enabled", true);
-#else
-pref("signon.management.page.os-auth.enabled", false);
-#endif
// "available" - user can see feature offer.
// "offered" - we have offered feature to user and they have not yet made a decision.
// "enabled" - user opted in to the feature.
@@ -2394,8 +2415,6 @@ pref("browser.crashReports.unsubmittedCheck.autoSubmit2", false);
// Preferences for the form autofill toolkit component.
// Checkbox in sync options for credit card data sync service
pref("services.sync.engine.creditcards.available", true);
-// Whether the user enabled the OS re-auth dialog.
-pref("extensions.formautofill.reauth.enabled", false);
// Whether or not to restore a session with lazy-browser tabs.
pref("browser.sessionstore.restore_tabs_lazily", true);
@@ -2406,11 +2425,7 @@ pref("browser.suppress_first_window_animation", true);
pref("extensions.screenshots.disabled", false);
// Preference that determines whether Screenshots uses the dedicated browser component
-#ifdef NIGHTLY_BUILD
- pref("screenshots.browser.component.enabled", true);
-#else
- pref("screenshots.browser.component.enabled", false);
-#endif
+pref("screenshots.browser.component.enabled", true);
// Preference that determines what button to focus
pref("screenshots.browser.component.last-saved-method", "download");
@@ -2510,9 +2525,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 +2532,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 +2563,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 +3042,22 @@ 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 control the visibility of the backup section in about:preferences
+pref("browser.backup.preferences.ui.enabled", false);
+// The number of SQLite database pages to backup per step.
+pref("browser.backup.sqlite.pages_per_step", 5);
+// The delay between SQLite database backup steps in milliseconds.
+pref("browser.backup.sqlite.step_delay_ms", 250);
// 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);
+// Pref to enable the ingestion through the Rust component.
+pref("toolkit.contentRelevancy.ingestEnabled", false);
diff --git a/browser/app/winlauncher/test/TestSameBinary.cpp b/browser/app/winlauncher/test/TestSameBinary.cpp
index 2cb45f546f..2adc85f4ec 100644
--- a/browser/app/winlauncher/test/TestSameBinary.cpp
+++ b/browser/app/winlauncher/test/TestSameBinary.cpp
@@ -193,8 +193,7 @@ static int ParentMain(int argc, wchar_t* argv[]) {
return 1;
}
- MOZ_ASSERT_UNREACHABLE("This process should be terminated by now");
- return 0;
+ MOZ_CRASH("This process should be terminated by now");
}
static int MonitorMain(int argc, wchar_t* argv[]) {
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&#38;utm_medium=referral&#38;utm_campaign=firefox_about&#38;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&#38;utm_medium=firefox-desktop&#38;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..9633c7d79d 100644
--- a/browser/base/content/appmenu-viewcache.inc.xhtml
+++ b/browser/base/content/appmenu-viewcache.inc.xhtml
@@ -3,24 +3,11 @@
# 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"
+ <panelview id="appMenu-mainView" class="PanelUI-subView"
lockpanelvertical="true">
<vbox class="panel-subview-body">
- <vbox id="appMenu-proton-addon-banners"/>
- <toolbarbutton id="appMenu-proton-update-banner" class="panel-banner-item subviewbutton"
- oncommand="PanelUI._onBannerItemSelected(event)"
+ <vbox id="appMenu-addon-banners"/>
+ <toolbarbutton id="appMenu-update-banner" class="panel-banner-item subviewbutton"
wrap="true"
hidden="true"/>
<toolbaritem id="appMenu-fxa-status2"
@@ -29,7 +16,7 @@
<html:div id="appMenu-fxa-text" data-l10n-id="appmenu-fxa-sync-and-save-data2"/>
<toolbarbutton id="appMenu-fxa-label2"
class="subviewbutton"
- oncommand="gSync.toggleAccountPanel(this, event)">
+ >
<vbox flex="1">
<label id="appMenu-header-title"
crop="end"/>
@@ -43,7 +30,6 @@
data-l10n-id="appmenuitem-profiles"
data-l10n-args='{ "profilename": "" }'
closemenu="none"
- oncommand="gProfiles.updateView(this)"
hidden="true"/>
<toolbarseparator id="appMenu-fxa-separator" class="proton-zap"/>
<toolbarbutton id="appMenu-new-tab-button2"
@@ -66,12 +52,12 @@
class="subviewbutton subviewbutton-nav"
data-l10n-id="library-bookmarks-menu"
closemenu="none"
- oncommand="BookmarkingUI.showSubView(this);"/>
+ />
<toolbarbutton id="appMenu-history-button"
class="subviewbutton subviewbutton-nav"
data-l10n-id="appmenuitem-history"
closemenu="none"
- oncommand="PanelUI.showSubView('PanelUI-history', this)"/>
+ />
<toolbarbutton id="appMenu-downloads-button"
class="subviewbutton"
data-l10n-id="appmenuitem-downloads"
@@ -80,7 +66,6 @@
<toolbarbutton id="appMenu-passwords-button"
class="subviewbutton"
data-l10n-id="appmenuitem-passwords"
- oncommand="LoginHelper.openPasswordManager(window, { entryPoint: 'mainmenu' })"
/>
<toolbarbutton id="appMenu-extensions-themes-button"
class="subviewbutton"
@@ -129,13 +114,6 @@
class="subviewbutton subviewbutton-iconic"
data-l10n-id="appmenuitem-fullscreen"
type="checkbox"
-# Note that we're custom-handling this click to make sure the panel disappears
-# before entering fullscreen, as it does some odd moving about on the screen
-# in the middle of the fullscreen transition otherwise.
- oncommand="
- this.closest('panel').hidePopup();
- setTimeout(() => BrowserFullScreen(), 0);
- "
tooltip="dynamic-shortcut-tooltip">
<observes element="View:FullScreen" attribute="checked"/>
</toolbarbutton>
@@ -147,12 +125,12 @@
#ifdef XP_MACOSX
key="key_preferencesCmdMac"
#endif
- oncommand="openPreferences()"/>
+ />
<toolbarbutton id="appMenu-more-button2"
class="subviewbutton subviewbutton-nav"
data-l10n-id="appmenuitem-more-tools"
closemenu="none"
- oncommand="PanelUI.showMoreToolsPanel(this);"/>
+ />
<toolbarbutton id="appMenu-report-broken-site-button"
class="subviewbutton subviewbutton-nav"
data-l10n-id="appmenuitem-report-broken-site"
@@ -163,7 +141,7 @@
class="subviewbutton subviewbutton-nav"
data-l10n-id="appmenuitem-help"
closemenu="none"
- oncommand="PanelUI.showSubView('PanelUI-helpView', this)"/>
+ />
#ifndef XP_MACOSX
<toolbarseparator/>
<toolbarbutton id="appMenu-quit-button2"
@@ -181,16 +159,16 @@
data-l10n-id="appmenu-recently-closed-tabs"
class="subviewbutton subviewbutton-nav"
closemenu="none"
- oncommand="PanelUI.showSubView('appMenu-library-recentlyClosedTabs', this)"/>
+ />
<toolbarbutton id="appMenuRecentlyClosedWindows"
data-l10n-id="appmenu-recently-closed-windows"
class="subviewbutton subviewbutton-nav"
closemenu="none"
- oncommand="PanelUI.showSubView('appMenu-library-recentlyClosedWindows', this)"/>
+ />
<toolbarbutton id="appMenuSearchHistory"
data-l10n-id="appmenu-search-history"
class="subviewbutton"
- oncommand="PlacesCommandHook.searchHistory()"/>
+ />
<toolbarbutton id="appMenu-restoreSession"
data-l10n-id="appmenu-restore-session"
class="subviewbutton"
@@ -216,7 +194,7 @@
<toolbarbutton id="PanelUI-historyMore"
class="subviewbutton panel-subview-footer-button"
data-l10n-id="appmenu-manage-history"
- oncommand="PlacesCommandHook.showPlacesOrganizer('History'); CustomizableUI.hidePanelForNode(this);"/>
+ />
</panelview>
<panelview id="PanelUI-profiles" flex="1">
@@ -227,10 +205,10 @@
<hbox id="this-profile-buttons">
<toolbarbutton id="profiles-edit-this-delete-button"
class="subviewbutton toolbarbutton-1"
- oncommand="switchToTabHavingURI('about:profilemanager', true)"/>
+ />
<toolbarbutton id="profiles-delete-this-profile-button"
class="subviewbutton toolbarbutton-1"
- oncommand="switchToTabHavingURI('about:profilemanager', true)"/>
+ />
</hbox>
</vbox>
<toolbarseparator/>
@@ -240,15 +218,15 @@
class="subviewbutton"
data-l10n-id="appmenu-close-profile"
data-l10n-args='{ "profilename": "" }'
- oncommand=""/>
+ />
<toolbarbutton id="profiles-create-profile-button"
class="subviewbutton"
data-l10n-id="appmenu-create-profile"
- oncommand="switchToTabHavingURI('about:profilemanager', true)"/>
+ />
<toolbarbutton id="profiles-manage-profiles-button"
class="subviewbutton"
data-l10n-id="appmenu-manage-profiles"
- oncommand="switchToTabHavingURI('about:profilemanager', true)"/>
+ />
</vbox>
</panelview>
@@ -272,12 +250,12 @@
<toolbarbutton id="panelMenu_searchBookmarks"
data-l10n-id="bookmarks-search"
class="subviewbutton"
- oncommand="PlacesCommandHook.searchBookmarks();"/>
+ />
<toolbarbutton id="panelMenu_viewBookmarksToolbar"
class="subviewbutton"
data-l10n-id="bookmarks-tools-toolbar-visibility-panel"
data-l10n-args='{ "isVisible": false }'
- oncommand="BookmarkingUI.toggleBookmarksToolbar('bookmark-tools');"/>
+ />
<toolbarseparator/>
<html:h2 id="panelMenu_recentBookmarks"
data-l10n-id="bookmarks-recent-bookmarks-panel-subheader"
@@ -470,8 +448,6 @@
<toolbarbutton id="PanelUI-remotetabs-syncnow"
align="center"
class="subviewbutton"
- oncommand="gSync.doSync();"
- onmouseover="gSync.refreshSyncButtonsTooltip();"
closemenu="none">
<hbox flex="1">
<image class="syncNowBtn"/>
@@ -486,7 +462,7 @@
<toolbarbutton id="PanelUI-remotetabs-view-managedevices"
class="subviewbutton"
data-l10n-id="appmenuitem-fxa-manage-account"
- oncommand="gSync.openDevicesManagementPage('syncedtabs-menupanel');">
+ >
<observes element="sidebar-box" attribute="positionend"/>
</toolbarbutton>
<toolbarseparator id="PanelUI-remotetabs-separator"/>
@@ -513,7 +489,7 @@
<toolbarbutton class="PanelUI-remotetabs-button"
id="PanelUI-remotetabs-tabsdisabledpane-button"
data-l10n-id="appmenu-remote-tabs-opensettings"
- oncommand="gSync.openPrefs('synced-tabs');"/>
+ />
</hbox>
</vbox>
</hbox>
@@ -527,7 +503,7 @@
<toolbarbutton id="PanelUI-remotetabs-connect-device-button"
class="PanelUI-remotetabs-button"
data-l10n-id="appmenu-remote-tabs-connectdevice"
- oncommand="gSync.openConnectAnotherDevice('synced-tabs');"/>
+ />
</vbox>
</hbox>
</deck>
@@ -545,7 +521,7 @@
<toolbarbutton class="PanelUI-remotetabs-button"
id="PanelUI-remotetabs-setupsync-button"
data-l10n-id="appmenu-remote-tabs-sign-into-sync"
- oncommand="gSync.openPrefs('synced-tabs');"/>
+ />
</vbox>
<!-- When Sync is not enabled -->
<vbox id="PanelUI-remotetabs-syncdisabled"
@@ -558,7 +534,7 @@
<toolbarbutton class="PanelUI-remotetabs-button"
id="PanelUI-remotetabs-syncdisabled-button"
data-l10n-id="appmenu-remote-tabs-turn-on-sync"
- oncommand="gSync.openPrefs('synced-tabs');"/>
+ />
</vbox>
<!-- When Sync needs re-authentication -->
<vbox id="PanelUI-remotetabs-reauthsync"
@@ -571,7 +547,7 @@
<toolbarbutton class="PanelUI-remotetabs-button"
id="PanelUI-remotetabs-reauthsync-button"
data-l10n-id="appmenu-remote-tabs-sign-into-sync"
- oncommand="gSync.openPrefs('synced-tabs');"/>
+ />
</vbox>
<!-- When Sync needs verification -->
<vbox id="PanelUI-remotetabs-unverified"
@@ -584,7 +560,7 @@
<toolbarbutton class="PanelUI-remotetabs-button"
id="PanelUI-remotetabs-unverified-button"
data-l10n-id="appmenu-remote-tabs-opensettings"
- oncommand="gSync.openPrefs('synced-tabs');"/>
+ />
</vbox>
</hbox>
</vbox>
@@ -595,7 +571,7 @@
<toolbarbutton id="fxa-manage-account-button"
align="center"
class="subviewbutton"
- oncommand="gSync.clickFxAMenuHeaderButton(this);">
+ >
<vbox flex="1">
<label id="fxa-menu-header-title"
crop="end"
@@ -609,8 +585,6 @@
<toolbarbutton id="PanelUI-fxa-menu-syncnow-button"
align="center"
class="subviewbutton"
- oncommand="gSync.doSyncFromFxaMenu(this);"
- onmouseover="gSync.refreshSyncButtonsTooltip();"
closemenu="none">
<hbox flex="1">
<image id="PanelUI-appMenu-fxa-image-last-synced"
@@ -627,43 +601,34 @@
<toolbarbutton id="PanelUI-fxa-menu-setup-sync-button"
class="subviewbutton"
data-l10n-id="appmenu-fxa-setup-sync"
- oncommand="gSync.openPrefsFromFxaMenu('sync_settings', this);"/>
+ />
<!-- The `Connect Another Device` button is disabled by default until the user logs into Sync. -->
<toolbarbutton id="PanelUI-fxa-menu-connect-device-button"
class="subviewbutton"
data-l10n-id="fxa-menu-connect-another-device"
disabled="true"
- oncommand="gSync.openConnectAnotherDeviceFromFxaMenu(this);"/>
+ />
<toolbarbutton id="PanelUI-fxa-menu-sendtab-button"
class="subviewbutton subviewbutton-nav"
data-l10n-id="fxa-menu-send-tab-to-device"
data-l10n-args='{"tabCount":1}'
closemenu="none"
- oncommand="gSync.showSendToDeviceViewFromFxaMenu(this);"/>
+ />
<toolbarbutton id="PanelUI-fxa-menu-sync-prefs-button"
class="subviewbutton"
data-l10n-id="fxa-menu-sync-settings"
hidden="true"
- oncommand="gSync.openPrefsFromFxaMenu('sync_settings', this);"/>
+ />
<toolbarseparator id="PanelUI-sign-out-separator" />
<toolbarbutton id="PanelUI-fxa-menu-account-signout-button"
class="subviewbutton"
data-l10n-id="fxa-menu-sign-out"
- oncommand="gSync.disconnect();"
hidden="true"/>
</vbox>
<!-- updateCTAPanel will control if we show this panel -->
- <vbox id="PanelUI-fxa-cta-menu">
- <toolbarbutton id="PanelUI-fxa-menu-sync-button" class="subviewbutton subviewbutton-iconic"
- oncommand="gSync.openPrefsFromFxaButton('sync_cta', this);">
- <vbox flex="1">
- <label id="fxa-menu-header-title" crop="end" data-l10n-id="fxa-menu-sync-title" />
- <label id="cta-menu-header-description" crop="end" data-l10n-id="fxa-menu-sync-description" />
- </vbox>
- </toolbarbutton>
+ <vbox id="PanelUI-fxa-cta-menu" hidden="true">
<toolbarseparator id="PanelUI-products-separator" />
- <toolbarbutton id="PanelUI-fxa-menu-monitor-button" class="subviewbutton subviewbutton-iconic"
- oncommand="gSync.openMonitorLink(this)">
+ <toolbarbutton id="PanelUI-fxa-menu-monitor-button" class="fxa-cta-button subviewbutton subviewbutton-iconic">
<vbox flex="1">
<hbox align="center">
<image class="PanelUI-fxa-menu-monitor-button ctaMenuLogo" role="presentation" />
@@ -672,8 +637,7 @@
<label id="cta-menu-header-description" crop="end" data-l10n-id="appmenuitem-monitor-description" />
</vbox>
</toolbarbutton>
- <toolbarbutton id="PanelUI-fxa-menu-relay-button" class="subviewbutton subviewbutton-iconic"
- oncommand="gSync.openRelayLink(this)">
+ <toolbarbutton id="PanelUI-fxa-menu-relay-button" class="fxa-cta-button subviewbutton subviewbutton-iconic">
<vbox flex="1">
<hbox align="center">
<image class="PanelUI-fxa-menu-relay-button ctaMenuLogo" role="presentation" />
@@ -682,8 +646,7 @@
<label id="cta-menu-header-description" crop="end" data-l10n-id="appmenuitem-relay-description" />
</vbox>
</toolbarbutton>
- <toolbarbutton id="PanelUI-fxa-menu-vpn-button" class="subviewbutton subviewbutton-iconic"
- oncommand="gSync.openVPNLink(this)">
+ <toolbarbutton id="PanelUI-fxa-menu-vpn-button" class="fxa-cta-button subviewbutton subviewbutton-iconic">
<vbox flex="1">
<hbox align="center">
<image class="PanelUI-fxa-menu-vpn-button ctaMenuLogo" role="presentation" />
@@ -725,7 +688,7 @@
<toolbarbutton id="PanelUI-fxa-menu-sendtab-not-configured-button"
class="PanelUI-fxa-signin-button"
data-l10n-id="appmenuitem-fxa-sign-in"
- oncommand="gSync.openPrefsFromFxaMenu('send_tab', this);"/>
+ />
</vbox>
</panelview>
@@ -736,7 +699,15 @@
<toolbarbutton id="PanelUI-fxa-menu-sendtab-connect-device-button"
class="PanelUI-fxa-signin-button"
data-l10n-id="appmenu-remote-tabs-connectdevice"
- oncommand="gSync.openConnectAnotherDeviceFromFxaMenu(this);"/>
+ />
+ </vbox>
+ </panelview>
+
+ <!-- This panelview holds the list of "inactive" tabs for devices -->
+ <panelview id="PanelUI-fxa-menu-inactive-tabs" class="PanelUI-subView PanelUI-remotetabs-clientcontainer">
+ <label itemtype="client">
+ </label>
+ <vbox class="panel-subview-body">
</vbox>
</panelview>
@@ -746,40 +717,17 @@
class="subviewbutton subviewbutton-nav"
data-l10n-id="library-bookmarks-menu"
closemenu="none"
- oncommand="BookmarkingUI.showSubView(this);"/>
+ />
<toolbarbutton id="appMenu-library-history-button"
class="subviewbutton subviewbutton-nav"
data-l10n-id="appmenuitem-history"
closemenu="none"
- oncommand="PanelUI.showSubView('PanelUI-history', this)"/>
+ />
<toolbarbutton id="appMenu-library-downloads-button"
class="subviewbutton"
data-l10n-id="appmenuitem-downloads"
- oncommand="DownloadsPanel.showDownloadsHistory();"/>
- </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">
@@ -795,15 +743,29 @@
<button id="reset-pbm-panel-cancel-button"
class="footer-button"
data-l10n-id="reset-pbm-panel-cancel-button"
- oncommand="ResetPBMPanel.onCancel(this)"></button>
+ ></button>
<button slot="primary"
id="reset-pbm-panel-confirm-button"
class="footer-button"
data-l10n-id="reset-pbm-panel-confirm-button"
- oncommand="ResetPBMPanel.onConfirm(this)"></button>
+ ></button>
</html:moz-button-group>
</vbox>
</panelview>
+ <panelview id="content-analysis-panel" class="PanelUI-subView" role="document" mainview-with-header="true">
+ <vbox id="content-analysis-panel-container" role="alertdialog" aria-labelledby="content-analysis-header">
+ <hbox class="panel-header">
+ <html:h1 id="content-analysis-header" data-l10n-id="content-analysis-panel-title"/>
+ </hbox>
+ <description id="content-analysis-panel-description">
+ <html:a is="moz-support-link"
+ data-l10n-name="info"
+ class="learnMore"
+ support-page="data-loss-prevention"/>
+ </description>
+ </vbox>
+ </panelview>
+
#include ../../components/reportbrokensite/content/reportBrokenSitePanel.inc.xhtml
</html:template>
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..e00952b2dc 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;
@@ -618,7 +618,6 @@ var gXPInstallObserver = {
break;
}
case "addon-install-blocked": {
- await window.ensureCustomElements("moz-support-link");
// Dismiss the progress notification. Note that this is bad if
// there are multiple simultaneous installs happening, see
// bug 1329884 for a longer explanation.
@@ -914,9 +913,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 +1008,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 +1021,7 @@ var gExtensionsNotifications = {
this._createAddonButton(
"webext-perms-update-menu-item",
update.addon,
- evt => {
+ () => {
ExtensionsUI.showUpdate(gBrowser, update);
}
);
@@ -1032,7 +1031,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 +1045,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 +1076,7 @@ var BrowserAddonUI = {
let result = confirmEx(
window,
title,
- message,
+ null,
btnFlags,
btnTitle,
/* button1 */ null,
@@ -1094,30 +1088,21 @@ var BrowserAddonUI = {
return { remove: result === 0, report: checkboxState.value };
},
- async reportAddon(addonId, reportEntryPoint) {
+ async reportAddon(addonId, _reportEntryPoint) {
let addon = addonId && (await AddonManager.getAddonByID(addonId));
if (!addon) {
return;
}
- // Do not open an additional about:addons tab if the abuse report should be
- // opened in its own tab.
- if (lazy.AbuseReporter.amoFormEnabled) {
- const amoUrl = lazy.AbuseReporter.getAMOFormURL({ addonId });
- window.openTrustedLinkIn(amoUrl, "tab", {
- // Make sure the newly open tab is going to be focused, independently
- // from general user prefs.
- forceForeground: true,
- });
- return;
- }
-
- const win = await BrowserOpenAddonsMgr("addons://list/extension");
-
- win.openAbuseReport({ addonId, reportEntryPoint });
+ const amoUrl = lazy.AbuseReporter.getAMOFormURL({ addonId });
+ window.openTrustedLinkIn(amoUrl, "tab", {
+ // Make sure the newly open tab is going to be focused, independently
+ // from general user prefs.
+ forceForeground: true,
+ });
},
- async removeAddon(addonId, eventObject) {
+ async removeAddon(addonId) {
let addon = addonId && (await AddonManager.getAddonByID(addonId));
if (!addon || !(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
return;
@@ -1136,13 +1121,91 @@ var BrowserAddonUI = {
}
},
- async manageAddon(addonId, eventObject) {
+ async manageAddon(addonId) {
let addon = addonId && (await AddonManager.getAddonByID(addonId));
if (!addon) {
return;
}
- BrowserOpenAddonsMgr("addons://detail/" + encodeURIComponent(addon.id));
+ this.openAddonsMgr("addons://detail/" + encodeURIComponent(addon.id));
+ },
+
+ /**
+ * Open about:addons page by given view id.
+ * @param {String} aView
+ * View id of page that will open.
+ * e.g. "addons://discover/"
+ * @param {Object} options
+ * {
+ * selectTabByViewId: If true, if there is the tab opening page having
+ * same view id, select the tab. Else if the current
+ * page is blank, load on it. Otherwise, open a new
+ * tab, then load on it.
+ * If false, if there is the tab opening
+ * about:addoons page, select the tab and load page
+ * for view id on it. Otherwise, leave the loading
+ * behavior to switchToTabHavingURI().
+ * If no options, handles as false.
+ * }
+ * @returns {Promise} When the Promise resolves, returns window object loaded the
+ * view id.
+ */
+ openAddonsMgr(aView, { selectTabByViewId = false } = {}) {
+ return new Promise(resolve => {
+ let emWindow;
+ let browserWindow;
+
+ const receivePong = function (aSubject) {
+ const browserWin = aSubject.browsingContext.topChromeWindow;
+ if (!emWindow || browserWin == window /* favor the current window */) {
+ if (
+ selectTabByViewId &&
+ aSubject.gViewController.currentViewId !== aView
+ ) {
+ return;
+ }
+
+ emWindow = aSubject;
+ browserWindow = browserWin;
+ }
+ };
+ Services.obs.addObserver(receivePong, "EM-pong");
+ Services.obs.notifyObservers(null, "EM-ping");
+ Services.obs.removeObserver(receivePong, "EM-pong");
+
+ if (emWindow) {
+ if (aView && !selectTabByViewId) {
+ emWindow.loadView(aView);
+ }
+ let tab = browserWindow.gBrowser.getTabForBrowser(
+ emWindow.docShell.chromeEventHandler
+ );
+ browserWindow.gBrowser.selectedTab = tab;
+ emWindow.focus();
+ resolve(emWindow);
+ return;
+ }
+
+ if (selectTabByViewId) {
+ const target = isBlankPageURL(gBrowser.currentURI.spec)
+ ? "current"
+ : "tab";
+ openTrustedLinkIn("about:addons", target);
+ } else {
+ // This must be a new load, else the ping/pong would have
+ // found the window above.
+ switchToTabHavingURI("about:addons", true);
+ }
+
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ Services.obs.removeObserver(observer, aTopic);
+ if (aView) {
+ aSubject.loadView(aView);
+ }
+ aSubject.focus();
+ resolve(aSubject);
+ }, "EM-loaded");
+ });
},
};
@@ -1556,7 +1619,7 @@ var gUnifiedExtensions = {
} else {
viewID = "addons://list/extension";
}
- await BrowserOpenAddonsMgr(viewID);
+ await BrowserAddonUI.openAddonsMgr(viewID);
return;
}
}
@@ -1798,7 +1861,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 +1876,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 +1886,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) {
@@ -1882,8 +1945,6 @@ var gUnifiedExtensions = {
supportPage = null,
type = "warning",
}) {
- window.ensureCustomElements("moz-message-bar");
-
const messageBar = document.createElement("moz-message-bar");
messageBar.setAttribute("type", type);
messageBar.classList.add("unified-extensions-message-bar");
@@ -1891,8 +1952,6 @@ var gUnifiedExtensions = {
messageBar.setAttribute("data-l10n-attrs", "heading, message");
if (supportPage) {
- window.ensureCustomElements("moz-support-link");
-
const supportUrl = document.createElement("a", {
is: "moz-support-link",
});
diff --git a/browser/base/content/browser-allTabsMenu.inc.xhtml b/browser/base/content/browser-allTabsMenu.inc.xhtml
index 71d26288f7..1be6576605 100644
--- a/browser/base/content/browser-allTabsMenu.inc.xhtml
+++ b/browser/base/content/browser-allTabsMenu.inc.xhtml
@@ -9,6 +9,10 @@
class="subviewbutton"
oncommand="gTabsPanel.searchTabs();"
data-l10n-id="all-tabs-menu-search-tabs"/>
+ <toolbarbutton id="allTabsMenu-closeDuplicateTabs"
+ class="subviewbutton"
+ oncommand="gBrowser.removeAllDuplicateTabs();"
+ data-l10n-id="all-tabs-menu-close-duplicate-tabs"/>
<toolbarbutton id="allTabsMenu-containerTabsButton"
class="subviewbutton subviewbutton-nav"
closemenu="none"
diff --git a/browser/base/content/browser-allTabsMenu.js b/browser/base/content/browser-allTabsMenu.js
index f11d4da71d..f4b15bc9c3 100644
--- a/browser/base/content/browser-allTabsMenu.js
+++ b/browser/base/content/browser-allTabsMenu.js
@@ -6,6 +6,7 @@
/* eslint-env mozilla/browser-window */
ChromeUtils.defineESModuleGetters(this, {
+ BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
TabsPanel: "resource:///modules/TabsList.sys.mjs",
});
@@ -58,7 +59,7 @@ var gTabsPanel = {
dropIndicator: this.dropIndicator,
});
- this.allTabsView.addEventListener("ViewShowing", e => {
+ this.allTabsView.addEventListener("ViewShowing", () => {
PanelUI._ensureShortcutsShown(this.allTabsView);
let containersEnabled =
@@ -72,9 +73,19 @@ var gTabsPanel = {
!hasHiddenTabs;
document.getElementById("allTabsMenu-hiddenTabsSeparator").hidden =
!hasHiddenTabs;
+
+ let closeDuplicateEnabled = Services.prefs.getBoolPref(
+ "browser.tabs.context.close-duplicate.enabled"
+ );
+ let closeDuplicateTabsItem = document.getElementById(
+ "allTabsMenu-closeDuplicateTabs"
+ );
+ closeDuplicateTabsItem.hidden = !closeDuplicateEnabled;
+ closeDuplicateTabsItem.disabled =
+ !closeDuplicateEnabled || !gBrowser.getAllDuplicateTabsToClose().length;
});
- this.allTabsView.addEventListener("ViewShown", e =>
+ this.allTabsView.addEventListener("ViewShown", () =>
this.allTabsView
.querySelector(".all-tabs-item[selected]")
?.scrollIntoView({ block: "center" })
@@ -149,6 +160,10 @@ var gTabsPanel = {
entrypoint,
1
);
+ BrowserUsageTelemetry.recordInteractionEvent(
+ entrypoint,
+ "all-tabs-panel-entrypoint"
+ );
PanelUI.showSubView(
this.kElements.allTabsView,
this.allTabsButton,
@@ -170,7 +185,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..b030891144 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-main id="sidebar-main" flex="1" hidden="true"></html:sidebar-main>
<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">
@@ -12,7 +13,7 @@
</toolbarbutton>
<image id="sidebar-throbber"/>
<spacer id="sidebar-spacer"/>
- <toolbarbutton id="sidebar-close" class="close-icon tabbable" data-l10n-id="sidebar-close-button" oncommand="SidebarUI.hide();"/>
+ <toolbarbutton id="sidebar-close" class="close-icon tabbable" data-l10n-id="sidebar-close-button" oncommand="SidebarController.hide();"/>
</box>
<browser id="sidebar" autoscroll="false" disablehistory="true" disablefullscreen="true" tooltip="aHTMLTooltip"/>
</vbox>
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..d80f9588cd
--- /dev/null
+++ b/browser/base/content/browser-commands.js
@@ -0,0 +1,591 @@
+/* -*- 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 = BrowserUtils.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;
+ },
+
+ downloadsUI() {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ openTrustedLinkIn("about:downloads", "tab");
+ } else {
+ PlacesCommandHook.showPlacesOrganizer("Downloads");
+ }
+ },
+
+ forceEncodingDetection() {
+ gBrowser.selectedBrowser.forceEncodingDetection();
+ BrowserCommands.reloadWithFlags(
+ Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE
+ );
+ },
+};
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-init.js b/browser/base/content/browser-init.js
new file mode 100644
index 0000000000..0717ce2138
--- /dev/null
+++ b/browser/base/content/browser-init.js
@@ -0,0 +1,1107 @@
+/* -*- 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/. */
+
+let _resolveDelayedStartup;
+var delayedStartupPromise = new Promise(resolve => {
+ _resolveDelayedStartup = resolve;
+});
+
+var gBrowserInit = {
+ delayedStartupFinished: false,
+ domContentLoaded: false,
+
+ _tabToAdopt: undefined,
+ _firstContentWindowPaintDeferred: Promise.withResolvers(),
+ idleTasksFinished: Promise.withResolvers(),
+
+ _setupFirstContentWindowPaintPromise() {
+ let lastTransactionId = window.windowUtils.lastTransactionId;
+ let layerTreeListener = () => {
+ if (this.getTabToAdopt()) {
+ // Need to wait until we finish adopting the tab, or we might end
+ // up focusing the initial browser and then losing focus when it
+ // gets swapped out for the tab to adopt.
+ return;
+ }
+ removeEventListener("MozLayerTreeReady", layerTreeListener);
+ let listener = e => {
+ if (e.transactionId > lastTransactionId) {
+ window.removeEventListener("MozAfterPaint", listener);
+ this._firstContentWindowPaintDeferred.resolve();
+ }
+ };
+ addEventListener("MozAfterPaint", listener);
+ };
+ addEventListener("MozLayerTreeReady", layerTreeListener);
+ },
+
+ getTabToAdopt() {
+ if (this._tabToAdopt !== undefined) {
+ return this._tabToAdopt;
+ }
+
+ if (window.arguments && window.XULElement.isInstance(window.arguments[0])) {
+ this._tabToAdopt = window.arguments[0];
+
+ // Clear the reference of the tab being adopted from the arguments.
+ window.arguments[0] = null;
+ } else {
+ // There was no tab to adopt in the arguments, set _tabToAdopt to null
+ // to avoid checking it again.
+ this._tabToAdopt = null;
+ }
+
+ return this._tabToAdopt;
+ },
+
+ _clearTabToAdopt() {
+ this._tabToAdopt = null;
+ },
+
+ // Used to check if the new window is still adopting an existing tab as its first tab
+ // (e.g. from the WebExtensions internals).
+ isAdoptingTab() {
+ return !!this.getTabToAdopt();
+ },
+
+ onBeforeInitialXULLayout() {
+ this._setupFirstContentWindowPaintPromise();
+
+ updateBookmarkToolbarVisibility();
+
+ // Set a sane starting width/height for all resolutions on new profiles.
+ if (ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null)) {
+ // When the fingerprinting resistance is enabled, making sure that we don't
+ // have a maximum window to interfere with generating rounded window dimensions.
+ document.documentElement.setAttribute("sizemode", "normal");
+ } else if (!document.documentElement.hasAttribute("width")) {
+ const TARGET_WIDTH = 1280;
+ const TARGET_HEIGHT = 1040;
+ let width = Math.min(screen.availWidth * 0.9, TARGET_WIDTH);
+ let height = Math.min(screen.availHeight * 0.9, TARGET_HEIGHT);
+
+ document.documentElement.setAttribute("width", width);
+ document.documentElement.setAttribute("height", height);
+
+ if (width < TARGET_WIDTH && height < TARGET_HEIGHT) {
+ document.documentElement.setAttribute("sizemode", "maximized");
+ }
+ }
+ if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
+ const toolbarMenubar = document.getElementById("toolbar-menubar");
+ // set a default value
+ if (!toolbarMenubar.hasAttribute("autohide")) {
+ toolbarMenubar.setAttribute("autohide", true);
+ }
+ document.l10n.setAttributes(
+ toolbarMenubar,
+ "toolbar-context-menu-menu-bar-cmd"
+ );
+ toolbarMenubar.setAttribute("data-l10n-attrs", "toolbarname");
+ }
+
+ // Run menubar initialization first, to avoid TabsInTitlebar code picking
+ // up mutations from it and causing a reflow.
+ AutoHideMenubar.init();
+ // Update the chromemargin attribute so the window can be sized correctly.
+ window.TabBarVisibility.update();
+ TabsInTitlebar.init();
+
+ new LightweightThemeConsumer(document);
+
+ if (
+ Services.prefs.getBoolPref(
+ "toolkit.legacyUserProfileCustomizations.windowIcon",
+ false
+ )
+ ) {
+ document.documentElement.setAttribute("icon", "main-window");
+ }
+
+ // Call this after we set attributes that might change toolbars' computed
+ // text color.
+ ToolbarIconColor.init();
+ },
+
+ onDOMContentLoaded() {
+ // This needs setting up before we create the first remote browser.
+ window.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).XULBrowserWindow = window.XULBrowserWindow;
+ window.browserDOMWindow = new nsBrowserAccess();
+
+ gBrowser = window._gBrowser;
+ delete window._gBrowser;
+ gBrowser.init();
+
+ BrowserWindowTracker.track(window);
+
+ FirefoxViewHandler.init();
+
+ gNavToolbox.palette = document.getElementById(
+ "BrowserToolbarPalette"
+ ).content;
+ for (let area of CustomizableUI.areas) {
+ let type = CustomizableUI.getAreaType(area);
+ if (type == CustomizableUI.TYPE_TOOLBAR) {
+ let node = document.getElementById(area);
+ CustomizableUI.registerToolbarNode(node);
+ }
+ }
+ BrowserSearch.initPlaceHolder();
+
+ // Hack to ensure that the various initial pages favicon is loaded
+ // instantaneously, to avoid flickering and improve perceived performance.
+ this._callWithURIToLoad(uriToLoad => {
+ let url;
+ try {
+ url = Services.io.newURI(uriToLoad);
+ } catch (e) {
+ return;
+ }
+ let nonQuery = url.prePath + url.filePath;
+ if (nonQuery in gPageIcons) {
+ gBrowser.setIcon(gBrowser.selectedTab, gPageIcons[nonQuery]);
+ }
+ });
+
+ updateFxaToolbarMenu(gFxaToolbarEnabled, true);
+
+ updatePrintCommands(gPrintEnabled);
+
+ gUnifiedExtensions.init();
+
+ // Setting the focus will cause a style flush, it's preferable to call anything
+ // that will modify the DOM from within this function before this call.
+ this._setInitialFocus();
+
+ this.domContentLoaded = true;
+ },
+
+ onLoad() {
+ gBrowser.addEventListener("DOMUpdateBlockedPopups", gPopupBlockerObserver);
+ gBrowser.addEventListener(
+ "TranslationsParent:LanguageState",
+ FullPageTranslationsPanel
+ );
+ gBrowser.addEventListener(
+ "TranslationsParent:OfferTranslation",
+ FullPageTranslationsPanel
+ );
+ gBrowser.addTabsProgressListener(FullPageTranslationsPanel);
+
+ window.addEventListener("AppCommand", HandleAppCommandEvent, true);
+
+ // These routines add message listeners. They must run before
+ // loading the frame script to ensure that we don't miss any
+ // message sent between when the frame script is loaded and when
+ // the listener is registered.
+ CaptivePortalWatcher.init();
+ ZoomUI.init(window);
+
+ if (!gMultiProcessBrowser) {
+ // There is a Content:Click message manually sent from content.
+ gBrowser.tabpanels.addEventListener("click", contentAreaClick, {
+ capture: true,
+ mozSystemGroup: true,
+ });
+ }
+
+ // hook up UI through progress listener
+ gBrowser.addProgressListener(window.XULBrowserWindow);
+ gBrowser.addTabsProgressListener(window.TabsProgressListener);
+
+ SidebarController.init();
+
+ // We do this in onload because we want to ensure the button's state
+ // doesn't flicker as the window is being shown.
+ DownloadsButton.init();
+
+ // Certain kinds of automigration rely on this notification to complete
+ // their tasks BEFORE the browser window is shown. SessionStore uses it to
+ // restore tabs into windows AFTER important parts like gMultiProcessBrowser
+ // have been initialized.
+ Services.obs.notifyObservers(window, "browser-window-before-show");
+
+ if (!window.toolbar.visible) {
+ // adjust browser UI for popups
+ gURLBar.readOnly = true;
+ }
+
+ // Misc. inits.
+ gUIDensity.init();
+ TabletModeUpdater.init();
+ CombinedStopReload.ensureInitialized();
+ gPrivateBrowsingUI.init();
+ BrowserSearch.init();
+ BrowserPageActions.init();
+ if (gToolbarKeyNavEnabled) {
+ ToolbarKeyboardNavigator.init();
+ }
+
+ // Update UI if browser is under remote control.
+ gRemoteControl.updateVisualCue();
+
+ // If we are given a tab to swap in, take care of it before first paint to
+ // avoid an about:blank flash.
+ let tabToAdopt = this.getTabToAdopt();
+ if (tabToAdopt) {
+ let evt = new CustomEvent("before-initial-tab-adopted", {
+ bubbles: true,
+ });
+ gBrowser.tabpanels.dispatchEvent(evt);
+
+ // Stop the about:blank load
+ gBrowser.stop();
+
+ // Remove the speculative focus from the urlbar to let the url be formatted.
+ gURLBar.removeAttribute("focused");
+
+ let swapBrowsers = () => {
+ try {
+ gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, tabToAdopt);
+ } catch (e) {
+ console.error(e);
+ }
+
+ // Clear the reference to the tab once its adoption has been completed.
+ this._clearTabToAdopt();
+ };
+ if (tabToAdopt.linkedBrowser.isRemoteBrowser) {
+ // For remote browsers, wait for the paint event, otherwise the tabs
+ // are not yet ready and focus gets confused because the browser swaps
+ // out while tabs are switching.
+ addEventListener("MozAfterPaint", swapBrowsers, { once: true });
+ } else {
+ swapBrowsers();
+ }
+ }
+
+ // Wait until chrome is painted before executing code not critical to making the window visible
+ this._boundDelayedStartup = this._delayedStartup.bind(this);
+ window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
+
+ if (!PrivateBrowsingUtils.enabled) {
+ document.getElementById("Tools:PrivateBrowsing").hidden = true;
+ // Setting disabled doesn't disable the shortcut, so we just remove
+ // the keybinding.
+ document.getElementById("key_privatebrowsing").remove();
+ }
+
+ if (BrowserUIUtils.quitShortcutDisabled) {
+ document.getElementById("key_quitApplication").remove();
+ document.getElementById("menu_FileQuitItem").removeAttribute("key");
+
+ PanelMultiView.getViewNode(
+ document,
+ "appMenu-quit-button2"
+ )?.removeAttribute("key");
+ }
+
+ this._loadHandled = true;
+ },
+
+ _cancelDelayedStartup() {
+ window.removeEventListener("MozAfterPaint", this._boundDelayedStartup);
+ this._boundDelayedStartup = null;
+ },
+
+ _delayedStartup() {
+ let { TelemetryTimestamps } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryTimestamps.sys.mjs"
+ );
+ TelemetryTimestamps.add("delayedStartupStarted");
+
+ this._cancelDelayedStartup();
+
+ // Bug 1531854 - The hidden window is force-created here
+ // until all of its dependencies are handled.
+ Services.appShell.hiddenDOMWindow;
+
+ gBrowser.addEventListener(
+ "PermissionStateChange",
+ function () {
+ gIdentityHandler.refreshIdentityBlock();
+ gPermissionPanel.updateSharingIndicator();
+ },
+ true
+ );
+
+ this._handleURIToLoad();
+
+ Services.obs.addObserver(gIdentityHandler, "perm-changed");
+ Services.obs.addObserver(gRemoteControl, "devtools-socket");
+ Services.obs.addObserver(gRemoteControl, "marionette-listening");
+ Services.obs.addObserver(gRemoteControl, "remote-listening");
+ Services.obs.addObserver(
+ gSessionHistoryObserver,
+ "browser:purge-session-history"
+ );
+ Services.obs.addObserver(
+ gStoragePressureObserver,
+ "QuotaManager::StoragePressure"
+ );
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled");
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-started");
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked");
+ Services.obs.addObserver(
+ gXPInstallObserver,
+ "addon-install-fullscreen-blocked"
+ );
+ Services.obs.addObserver(
+ gXPInstallObserver,
+ "addon-install-origin-blocked"
+ );
+ Services.obs.addObserver(
+ gXPInstallObserver,
+ "addon-install-policy-blocked"
+ );
+ Services.obs.addObserver(
+ gXPInstallObserver,
+ "addon-install-webapi-blocked"
+ );
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-failed");
+ Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation");
+ Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup");
+
+ BrowserOffline.init();
+ CanvasPermissionPromptHelper.init();
+ WebAuthnPromptHelper.init();
+ ContentAnalysis.initialize(document);
+
+ // Initialize the full zoom setting.
+ // We do this before the session restore service gets initialized so we can
+ // apply full zoom settings to tabs restored by the session restore service.
+ FullZoom.init();
+ PanelUI.init(shouldSuppressPopupNotifications);
+ ReportBrokenSite.init(gBrowser);
+
+ UpdateUrlbarSearchSplitterState();
+
+ BookmarkingUI.init();
+ BrowserSearch.delayedStartupInit();
+ SearchUIUtils.init();
+ gProtectionsHandler.init();
+ HomePage.delayedStartup().catch(console.error);
+
+ let safeMode = document.getElementById("helpSafeMode");
+ if (Services.appinfo.inSafeMode) {
+ document.l10n.setAttributes(safeMode, "menu-help-exit-troubleshoot-mode");
+ safeMode.setAttribute(
+ "appmenu-data-l10n-id",
+ "appmenu-help-exit-troubleshoot-mode"
+ );
+ }
+
+ // BiDi UI
+ gBidiUI = isBidiEnabled();
+ if (gBidiUI) {
+ document.getElementById("documentDirection-separator").hidden = false;
+ document.getElementById("documentDirection-swap").hidden = false;
+ document.getElementById("textfieldDirection-separator").hidden = false;
+ document.getElementById("textfieldDirection-swap").hidden = false;
+ }
+
+ // Setup click-and-hold gestures access to the session history
+ // menus if global click-and-hold isn't turned on
+ if (!Services.prefs.getBoolPref("ui.click_hold_context_menus", false)) {
+ SetClickAndHoldHandlers();
+ }
+
+ function initBackForwardButtonTooltip(tooltipId, l10nId, shortcutId) {
+ let shortcut = document.getElementById(shortcutId);
+ shortcut = ShortcutUtils.prettifyShortcut(shortcut);
+
+ let tooltip = document.getElementById(tooltipId);
+ document.l10n.setAttributes(tooltip, l10nId, { shortcut });
+ }
+
+ initBackForwardButtonTooltip(
+ "back-button-tooltip-description",
+ "navbar-tooltip-back-2",
+ "goBackKb"
+ );
+
+ initBackForwardButtonTooltip(
+ "forward-button-tooltip-description",
+ "navbar-tooltip-forward-2",
+ "goForwardKb"
+ );
+
+ PlacesToolbarHelper.init();
+
+ ctrlTab.readPref();
+ Services.prefs.addObserver(ctrlTab.prefName, ctrlTab);
+
+ // The object handling the downloads indicator is initialized here in the
+ // delayed startup function, but the actual indicator element is not loaded
+ // unless there are downloads to be displayed.
+ DownloadsButton.initializeIndicator();
+
+ if (AppConstants.platform != "macosx") {
+ updateEditUIVisibility();
+ let placesContext = document.getElementById("placesContext");
+ placesContext.addEventListener("popupshowing", updateEditUIVisibility);
+ placesContext.addEventListener("popuphiding", updateEditUIVisibility);
+ }
+
+ FullScreen.init();
+ MenuTouchModeObserver.init();
+
+ if (AppConstants.MOZ_DATA_REPORTING) {
+ gDataNotificationInfoBar.init();
+ }
+
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ DevelopmentHelpers.init();
+ }
+
+ gExtensionsNotifications.init();
+
+ let wasMinimized = window.windowState == window.STATE_MINIMIZED;
+ window.addEventListener("sizemodechange", () => {
+ let isMinimized = window.windowState == window.STATE_MINIMIZED;
+ if (wasMinimized != isMinimized) {
+ wasMinimized = isMinimized;
+ UpdatePopupNotificationsVisibility();
+ }
+ });
+
+ window.addEventListener("mousemove", MousePosTracker);
+ window.addEventListener("dragover", MousePosTracker);
+
+ gNavToolbox.addEventListener("customizationstarting", CustomizationHandler);
+ gNavToolbox.addEventListener("aftercustomization", CustomizationHandler);
+
+ SessionStore.promiseInitialized.then(() => {
+ // Bail out if the window has been closed in the meantime.
+ if (window.closed) {
+ return;
+ }
+
+ // Enable the Restore Last Session command if needed
+ RestoreLastSessionObserver.init();
+
+ SidebarController.startDelayedLoad();
+
+ PanicButtonNotifier.init();
+ });
+
+ if (BrowserHandler.kiosk) {
+ // We don't modify popup windows for kiosk mode
+ if (!gURLBar.readOnly) {
+ window.fullScreen = true;
+ }
+ }
+
+ if (Services.policies.status === Services.policies.ACTIVE) {
+ if (!Services.policies.isAllowed("hideShowMenuBar")) {
+ document
+ .getElementById("toolbar-menubar")
+ .removeAttribute("toolbarname");
+ }
+ if (!Services.policies.isAllowed("filepickers")) {
+ let savePageCommand = document.getElementById("Browser:SavePage");
+ let openFileCommand = document.getElementById("Browser:OpenFile");
+
+ savePageCommand.setAttribute("disabled", "true");
+ openFileCommand.setAttribute("disabled", "true");
+
+ document.addEventListener("FilePickerBlocked", function (event) {
+ let browser = event.target;
+
+ let notificationBox = browser
+ .getTabBrowser()
+ ?.getNotificationBox(browser);
+
+ // Prevent duplicate notifications
+ if (
+ notificationBox &&
+ !notificationBox.getNotificationWithValue("filepicker-blocked")
+ ) {
+ notificationBox.appendNotification("filepicker-blocked", {
+ label: {
+ "l10n-id": "filepicker-blocked-infobar",
+ },
+ priority: notificationBox.PRIORITY_INFO_LOW,
+ });
+ }
+ });
+ }
+ let policies = Services.policies.getActivePolicies();
+ if ("ManagedBookmarks" in policies) {
+ let managedBookmarks = policies.ManagedBookmarks;
+ let children = managedBookmarks.filter(
+ child => !("toplevel_name" in child)
+ );
+ if (children.length) {
+ let managedBookmarksButton =
+ document.createXULElement("toolbarbutton");
+ managedBookmarksButton.setAttribute("id", "managed-bookmarks");
+ managedBookmarksButton.setAttribute("class", "bookmark-item");
+ let toplevel = managedBookmarks.find(
+ element => "toplevel_name" in element
+ );
+ if (toplevel) {
+ managedBookmarksButton.setAttribute(
+ "label",
+ toplevel.toplevel_name
+ );
+ } else {
+ document.l10n.setAttributes(
+ managedBookmarksButton,
+ "managed-bookmarks"
+ );
+ }
+ managedBookmarksButton.setAttribute("context", "placesContext");
+ managedBookmarksButton.setAttribute("container", "true");
+ managedBookmarksButton.setAttribute("removable", "false");
+ managedBookmarksButton.setAttribute("type", "menu");
+
+ let managedBookmarksPopup = document.createXULElement("menupopup");
+ managedBookmarksPopup.setAttribute("id", "managed-bookmarks-popup");
+ managedBookmarksPopup.setAttribute(
+ "oncommand",
+ "PlacesToolbarHelper.openManagedBookmark(event);"
+ );
+ managedBookmarksPopup.setAttribute(
+ "ondragover",
+ "event.dataTransfer.effectAllowed='none';"
+ );
+ managedBookmarksPopup.setAttribute(
+ "ondragstart",
+ "PlacesToolbarHelper.onDragStartManaged(event);"
+ );
+ managedBookmarksPopup.setAttribute(
+ "onpopupshowing",
+ "PlacesToolbarHelper.populateManagedBookmarks(this);"
+ );
+ managedBookmarksPopup.setAttribute("placespopup", "true");
+ managedBookmarksPopup.setAttribute("is", "places-popup");
+ managedBookmarksPopup.classList.add("toolbar-menupopup");
+ managedBookmarksButton.appendChild(managedBookmarksPopup);
+
+ gNavToolbox.palette.appendChild(managedBookmarksButton);
+
+ CustomizableUI.ensureWidgetPlacedInWindow(
+ "managed-bookmarks",
+ window
+ );
+
+ // Add button if it doesn't exist
+ if (!CustomizableUI.getPlacementOfWidget("managed-bookmarks")) {
+ CustomizableUI.addWidgetToArea(
+ "managed-bookmarks",
+ CustomizableUI.AREA_BOOKMARKS,
+ 0
+ );
+ }
+ }
+ }
+ }
+
+ CaptivePortalWatcher.delayedStartup();
+
+ ShoppingSidebarManager.ensureInitialized();
+
+ SessionStore.promiseAllWindowsRestored.then(() => {
+ this._schedulePerWindowIdleTasks();
+ document.documentElement.setAttribute("sessionrestored", "true");
+ });
+
+ this.delayedStartupFinished = true;
+ _resolveDelayedStartup();
+ Services.obs.notifyObservers(window, "browser-delayed-startup-finished");
+ TelemetryTimestamps.add("delayedStartupFinished");
+ // We've announced that delayed startup has finished. Do not add code past this point.
+ },
+
+ /**
+ * Resolved on the first MozLayerTreeReady and next MozAfterPaint in the
+ * parent process.
+ */
+ get firstContentWindowPaintPromise() {
+ return this._firstContentWindowPaintDeferred.promise;
+ },
+
+ _setInitialFocus() {
+ let initiallyFocusedElement = document.commandDispatcher.focusedElement;
+
+ // To prevent startup flicker, the urlbar has the 'focused' attribute set
+ // by default. If we are not sure the urlbar will be focused in this
+ // window, we need to remove the attribute before first paint.
+ // TODO (bug 1629956): The urlbar having the 'focused' attribute by default
+ // isn't a useful optimization anymore since UrlbarInput needs layout
+ // information to focus the urlbar properly.
+ let shouldRemoveFocusedAttribute = true;
+
+ this._callWithURIToLoad(uriToLoad => {
+ if (
+ isBlankPageURL(uriToLoad) ||
+ uriToLoad == "about:privatebrowsing" ||
+ this.getTabToAdopt()?.isEmpty
+ ) {
+ gURLBar.select();
+ shouldRemoveFocusedAttribute = false;
+ return;
+ }
+
+ // If the initial browser is remote, in order to optimize for first paint,
+ // we'll defer switching focus to that browser until it has painted.
+ // Otherwise use a regular promise to guarantee that mutationobserver
+ // microtasks that could affect focusability have run.
+ let promise = gBrowser.selectedBrowser.isRemoteBrowser
+ ? this.firstContentWindowPaintPromise
+ : Promise.resolve();
+
+ promise.then(() => {
+ // If focus didn't move while we were waiting, we're okay to move to
+ // the browser.
+ if (
+ document.commandDispatcher.focusedElement == initiallyFocusedElement
+ ) {
+ gBrowser.selectedBrowser.focus();
+ }
+ });
+ });
+
+ // Delay removing the attribute using requestAnimationFrame to avoid
+ // invalidating styles multiple times in a row if uriToLoadPromise
+ // resolves before first paint.
+ if (shouldRemoveFocusedAttribute) {
+ window.requestAnimationFrame(() => {
+ if (shouldRemoveFocusedAttribute) {
+ gURLBar.removeAttribute("focused");
+ }
+ });
+ }
+ },
+
+ _handleURIToLoad() {
+ this._callWithURIToLoad(uriToLoad => {
+ if (!uriToLoad) {
+ // We don't check whether window.arguments[5] (userContextId) is set
+ // because tabbrowser.js takes care of that for the initial tab.
+ return;
+ }
+
+ // We don't check if uriToLoad is a XULElement because this case has
+ // already been handled before first paint, and the argument cleared.
+ if (Array.isArray(uriToLoad)) {
+ // This function throws for certain malformed URIs, so use exception handling
+ // so that we don't disrupt startup
+ try {
+ gBrowser.loadTabs(uriToLoad, {
+ inBackground: false,
+ replace: true,
+ // See below for the semantics of window.arguments. Only the minimum is supported.
+ userContextId: window.arguments[5],
+ triggeringPrincipal:
+ window.arguments[8] ||
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ allowInheritPrincipal: window.arguments[9],
+ csp: window.arguments[10],
+ fromExternal: true,
+ });
+ } catch (e) {}
+ } else if (window.arguments.length >= 3) {
+ // window.arguments[1]: extraOptions (nsIPropertyBag)
+ // [2]: referrerInfo (nsIReferrerInfo)
+ // [3]: postData (nsIInputStream)
+ // [4]: allowThirdPartyFixup (bool)
+ // [5]: userContextId (int)
+ // [6]: originPrincipal (nsIPrincipal)
+ // [7]: originStoragePrincipal (nsIPrincipal)
+ // [8]: triggeringPrincipal (nsIPrincipal)
+ // [9]: allowInheritPrincipal (bool)
+ // [10]: csp (nsIContentSecurityPolicy)
+ // [11]: nsOpenWindowInfo
+ let userContextId =
+ window.arguments[5] != undefined
+ ? window.arguments[5]
+ : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+
+ let hasValidUserGestureActivation = undefined;
+ let fromExternal = undefined;
+ let globalHistoryOptions = undefined;
+ let triggeringRemoteType = undefined;
+ let forceAllowDataURI = false;
+ let wasSchemelessInput = false;
+ if (window.arguments[1]) {
+ if (!(window.arguments[1] instanceof Ci.nsIPropertyBag2)) {
+ throw new Error(
+ "window.arguments[1] must be null or Ci.nsIPropertyBag2!"
+ );
+ }
+
+ let extraOptions = window.arguments[1];
+ if (extraOptions.hasKey("hasValidUserGestureActivation")) {
+ hasValidUserGestureActivation = extraOptions.getPropertyAsBool(
+ "hasValidUserGestureActivation"
+ );
+ }
+ if (extraOptions.hasKey("fromExternal")) {
+ fromExternal = extraOptions.getPropertyAsBool("fromExternal");
+ }
+ if (extraOptions.hasKey("triggeringSponsoredURL")) {
+ globalHistoryOptions = {
+ triggeringSponsoredURL: extraOptions.getPropertyAsACString(
+ "triggeringSponsoredURL"
+ ),
+ };
+ if (extraOptions.hasKey("triggeringSponsoredURLVisitTimeMS")) {
+ globalHistoryOptions.triggeringSponsoredURLVisitTimeMS =
+ extraOptions.getPropertyAsUint64(
+ "triggeringSponsoredURLVisitTimeMS"
+ );
+ }
+ }
+ if (extraOptions.hasKey("triggeringRemoteType")) {
+ triggeringRemoteType = extraOptions.getPropertyAsACString(
+ "triggeringRemoteType"
+ );
+ }
+ if (extraOptions.hasKey("forceAllowDataURI")) {
+ forceAllowDataURI =
+ extraOptions.getPropertyAsBool("forceAllowDataURI");
+ }
+ if (extraOptions.hasKey("wasSchemelessInput")) {
+ wasSchemelessInput =
+ extraOptions.getPropertyAsBool("wasSchemelessInput");
+ }
+ }
+
+ try {
+ openLinkIn(uriToLoad, "current", {
+ referrerInfo: window.arguments[2] || null,
+ postData: window.arguments[3] || null,
+ allowThirdPartyFixup: window.arguments[4] || false,
+ userContextId,
+ // pass the origin principal (if any) and force its use to create
+ // an initial about:blank viewer if present:
+ originPrincipal: window.arguments[6],
+ originStoragePrincipal: window.arguments[7],
+ triggeringPrincipal: window.arguments[8],
+ // TODO fix allowInheritPrincipal to default to false.
+ // Default to true unless explicitly set to false because of bug 1475201.
+ allowInheritPrincipal: window.arguments[9] !== false,
+ csp: window.arguments[10],
+ forceAboutBlankViewerInCurrent: !!window.arguments[6],
+ forceAllowDataURI,
+ hasValidUserGestureActivation,
+ fromExternal,
+ globalHistoryOptions,
+ triggeringRemoteType,
+ wasSchemelessInput,
+ });
+ } catch (e) {
+ console.error(e);
+ }
+
+ window.focus();
+ } else {
+ // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3.
+ // Such callers expect that window.arguments[0] is handled as a single URI.
+ loadOneOrMoreURIs(
+ uriToLoad,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null
+ );
+ }
+ });
+ },
+
+ /**
+ * Use this function as an entry point to schedule tasks that
+ * need to run once per window after startup, and can be scheduled
+ * by using an idle callback.
+ *
+ * The functions scheduled here will fire from idle callbacks
+ * once every window has finished being restored by session
+ * restore, and after the equivalent only-once tasks
+ * have run (from _scheduleStartupIdleTasks in BrowserGlue.sys.mjs).
+ */
+ _schedulePerWindowIdleTasks() {
+ // Bail out if the window has been closed in the meantime.
+ if (window.closed) {
+ return;
+ }
+
+ function scheduleIdleTask(func, options) {
+ requestIdleCallback(function idleTaskRunner() {
+ if (!window.closed) {
+ func();
+ }
+ }, options);
+ }
+
+ scheduleIdleTask(() => {
+ // Initialize the Sync UI
+ gSync.init();
+ });
+
+ scheduleIdleTask(() => {
+ // Read prefers-reduced-motion setting
+ let reduceMotionQuery = window.matchMedia(
+ "(prefers-reduced-motion: reduce)"
+ );
+ function readSetting() {
+ gReduceMotionSetting = reduceMotionQuery.matches;
+ }
+ reduceMotionQuery.addListener(readSetting);
+ readSetting();
+ });
+
+ scheduleIdleTask(() => {
+ // setup simple gestures support
+ gGestureSupport.init(true);
+
+ // setup history swipe animation
+ gHistorySwipeAnimation.init();
+ });
+
+ scheduleIdleTask(() => {
+ gBrowserThumbnails.init();
+ });
+
+ scheduleIdleTask(
+ () => {
+ // Initialize the download manager some time after the app starts so that
+ // auto-resume downloads begin (such as after crashing or quitting with
+ // active downloads) and speeds up the first-load of the download manager UI.
+ // If the user manually opens the download manager before the timeout, the
+ // downloads will start right away, and initializing again won't hurt.
+ try {
+ DownloadsCommon.initializeAllDataLinks();
+ ChromeUtils.importESModule(
+ "resource:///modules/DownloadsTaskbar.sys.mjs"
+ ).DownloadsTaskbar.registerIndicator(window);
+ if (AppConstants.platform == "macosx") {
+ ChromeUtils.importESModule(
+ "resource:///modules/DownloadsMacFinderProgress.sys.mjs"
+ ).DownloadsMacFinderProgress.register();
+ }
+ Services.telemetry.setEventRecordingEnabled("downloads", true);
+ } catch (ex) {
+ console.error(ex);
+ }
+ },
+ { timeout: 10000 }
+ );
+
+ if (Win7Features) {
+ scheduleIdleTask(() => Win7Features.onOpenWindow());
+ }
+
+ scheduleIdleTask(async () => {
+ NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
+ });
+
+ scheduleIdleTask(() => {
+ gGfxUtils.init();
+ });
+
+ // This should always go last, since the idle tasks (except for the ones with
+ // timeouts) should execute in order. Note that this observer notification is
+ // not guaranteed to fire, since the window could close before we get here.
+ scheduleIdleTask(() => {
+ this.idleTasksFinished.resolve();
+ Services.obs.notifyObservers(
+ window,
+ "browser-idle-startup-tasks-finished"
+ );
+ });
+
+ scheduleIdleTask(() => {
+ gProfiles.init();
+ });
+ },
+
+ // Returns the URI(s) to load at startup if it is immediately known, or a
+ // promise resolving to the URI to load.
+ get uriToLoadPromise() {
+ delete this.uriToLoadPromise;
+ return (this.uriToLoadPromise = (function () {
+ // window.arguments[0]: URI to load (string), or an nsIArray of
+ // nsISupportsStrings to load, or a xul:tab of
+ // a tabbrowser, which will be replaced by this
+ // window (for this case, all other arguments are
+ // ignored).
+ let uri = window.arguments?.[0];
+ if (!uri || window.XULElement.isInstance(uri)) {
+ return null;
+ }
+
+ let defaultArgs = BrowserHandler.defaultArgs;
+
+ // If the given URI is different from the homepage, we want to load it.
+ if (uri != defaultArgs) {
+ AboutNewTab.noteNonDefaultStartup();
+
+ if (uri instanceof Ci.nsIArray) {
+ // Transform the nsIArray of nsISupportsString's into a JS Array of
+ // JS strings.
+ return Array.from(
+ uri.enumerate(Ci.nsISupportsString),
+ supportStr => supportStr.data
+ );
+ } else if (uri instanceof Ci.nsISupportsString) {
+ return uri.data;
+ }
+ return uri;
+ }
+
+ // The URI appears to be the the homepage. We want to load it only if
+ // session restore isn't about to override the homepage.
+ let willOverride = SessionStartup.willOverrideHomepage;
+ if (typeof willOverride == "boolean") {
+ return willOverride ? null : uri;
+ }
+ return willOverride.then(willOverrideHomepage =>
+ willOverrideHomepage ? null : uri
+ );
+ })());
+ },
+
+ // Calls the given callback with the URI to load at startup.
+ // Synchronously if possible, or after uriToLoadPromise resolves otherwise.
+ _callWithURIToLoad(callback) {
+ let uriToLoad = this.uriToLoadPromise;
+ if (uriToLoad && uriToLoad.then) {
+ uriToLoad.then(callback);
+ } else {
+ callback(uriToLoad);
+ }
+ },
+
+ onUnload() {
+ gUIDensity.uninit();
+
+ TabsInTitlebar.uninit();
+
+ ToolbarIconColor.uninit();
+
+ // In certain scenarios it's possible for unload to be fired before onload,
+ // (e.g. if the window is being closed after browser.js loads but before the
+ // load completes). In that case, there's nothing to do here.
+ if (!this._loadHandled) {
+ return;
+ }
+
+ // First clean up services initialized in gBrowserInit.onLoad (or those whose
+ // uninit methods don't depend on the services having been initialized).
+
+ CombinedStopReload.uninit();
+
+ gGestureSupport.init(false);
+
+ gHistorySwipeAnimation.uninit();
+
+ FullScreen.uninit();
+
+ gSync.uninit();
+
+ gExtensionsNotifications.uninit();
+ gUnifiedExtensions.uninit();
+
+ try {
+ gBrowser.removeProgressListener(window.XULBrowserWindow);
+ gBrowser.removeTabsProgressListener(window.TabsProgressListener);
+ } catch (ex) {}
+
+ PlacesToolbarHelper.uninit();
+
+ BookmarkingUI.uninit();
+
+ TabletModeUpdater.uninit();
+
+ gTabletModePageCounter.finish();
+
+ CaptivePortalWatcher.uninit();
+
+ SidebarController.uninit();
+
+ DownloadsButton.uninit();
+
+ if (gToolbarKeyNavEnabled) {
+ ToolbarKeyboardNavigator.uninit();
+ }
+
+ BrowserSearch.uninit();
+
+ NewTabPagePreloading.removePreloadedBrowser(window);
+
+ FirefoxViewHandler.uninit();
+
+ // Now either cancel delayedStartup, or clean up the services initialized from
+ // it.
+ if (this._boundDelayedStartup) {
+ this._cancelDelayedStartup();
+ } else {
+ if (Win7Features) {
+ Win7Features.onCloseWindow();
+ }
+ Services.prefs.removeObserver(ctrlTab.prefName, ctrlTab);
+ ctrlTab.uninit();
+ gBrowserThumbnails.uninit();
+ gProtectionsHandler.uninit();
+ FullZoom.destroy();
+
+ Services.obs.removeObserver(gIdentityHandler, "perm-changed");
+ Services.obs.removeObserver(gRemoteControl, "devtools-socket");
+ Services.obs.removeObserver(gRemoteControl, "marionette-listening");
+ Services.obs.removeObserver(gRemoteControl, "remote-listening");
+ Services.obs.removeObserver(
+ gSessionHistoryObserver,
+ "browser:purge-session-history"
+ );
+ Services.obs.removeObserver(
+ gStoragePressureObserver,
+ "QuotaManager::StoragePressure"
+ );
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
+ Services.obs.removeObserver(
+ gXPInstallObserver,
+ "addon-install-fullscreen-blocked"
+ );
+ Services.obs.removeObserver(
+ gXPInstallObserver,
+ "addon-install-origin-blocked"
+ );
+ Services.obs.removeObserver(
+ gXPInstallObserver,
+ "addon-install-policy-blocked"
+ );
+ Services.obs.removeObserver(
+ gXPInstallObserver,
+ "addon-install-webapi-blocked"
+ );
+ Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
+ Services.obs.removeObserver(
+ gXPInstallObserver,
+ "addon-install-confirmation"
+ );
+ Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup");
+
+ MenuTouchModeObserver.uninit();
+ BrowserOffline.uninit();
+ CanvasPermissionPromptHelper.uninit();
+ WebAuthnPromptHelper.uninit();
+ PanelUI.uninit();
+ }
+
+ // Final window teardown, do this last.
+ gBrowser.destroy();
+ window.XULBrowserWindow = null;
+ window.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow).XULBrowserWindow = null;
+ window.browserDOMWindow = null;
+ },
+};
+
+gBrowserInit.idleTasksFinishedPromise = gBrowserInit.idleTasksFinished.promise;
diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc
index 66dd662137..0eebfea75a 100644
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -140,19 +140,7 @@
</menupopup>
</menu>
<menu id="viewSidebarMenuMenu" data-l10n-id="menu-view-sidebar">
- <menupopup id="viewSidebarMenu">
- <menuitem id="menu_bookmarksSidebar"
- type="checkbox"
- key="viewBookmarksSidebarKb"
- oncommand="SidebarUI.toggle('viewBookmarksSidebar');" data-l10n-id="menu-view-bookmarks"/>
- <menuitem id="menu_historySidebar"
- type="checkbox"
- key="key_gotoHistory"
- oncommand="SidebarUI.toggle('viewHistorySidebar');" data-l10n-id="menu-view-history-button"/>
- <menuitem id="menu_tabsSidebar"
- type="checkbox"
- class="sync-ui-item"
- oncommand="SidebarUI.toggle('viewTabsSidebar');" data-l10n-id="menu-view-synced-tabs-sidebar"/>
+ <menupopup id="viewSidebarMenu" onpopupshowing="SidebarController.setMegalistMenubarVisibility(event);">
</menupopup>
</menu>
<menuseparator/>
@@ -189,7 +177,7 @@
</menu>
<menuitem id="repair-text-encoding"
disabled="true"
- oncommand="BrowserForceEncodingDetection();"
+ oncommand="BrowserCommands.forceEncodingDetection();"
data-l10n-id="menu-view-repair-text-encoding"/>
<menuseparator/>
#ifdef XP_MACOSX
diff --git a/browser/base/content/browser-pageActions.js b/browser/base/content/browser-pageActions.js
index 1cc895434d..fe0453160d 100644
--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -970,7 +970,7 @@ var BrowserPageActions = {
this._contextAction = null;
let viewID = "addons://detail/" + encodeURIComponent(action.extensionID);
- window.BrowserOpenAddonsMgr(viewID);
+ window.BrowserAddonUI.openAddonsMgr(viewID);
},
/**
@@ -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..404a080983 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")
@@ -55,7 +55,6 @@ var StarUI = {
delete this.panel;
this._createPanelIfNeeded();
var element = this._element("editBookmarkPanel");
- window.ensureCustomElements("moz-button-group");
// initially the panel is hidden
// to avoid impacting startup / new window performance
element.hidden = false;
@@ -253,7 +252,7 @@ var StarUI = {
}
target.addEventListener(
"popupshown",
- function (event) {
+ function () {
fn();
},
{ capture: true, once: true }
@@ -1174,7 +1173,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 +1182,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
@@ -1378,7 +1377,7 @@ var BookmarkingUI = {
this.updateLabel(
"BMB_viewBookmarksSidebar",
- SidebarUI.currentID == "viewBookmarksSidebar"
+ SidebarController.currentID == "viewBookmarksSidebar"
);
this.updateLabel("BMB_viewBookmarksToolbar", !this.toolbar.collapsed);
},
@@ -1659,13 +1658,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();
}
@@ -1999,6 +1998,13 @@ var BookmarkingUI = {
case "ViewHiding":
this.onPanelMenuViewHiding(aEvent);
break;
+ case "command":
+ if (aEvent.target.id == "panelMenu_searchBookmarks") {
+ PlacesCommandHook.searchBookmarks();
+ } else if (aEvent.target.id == "panelMenu_viewBookmarksToolbar") {
+ this.toggleBookmarksToolbar("bookmark-tools");
+ }
+ break;
}
},
@@ -2026,12 +2032,15 @@ var BookmarkingUI = {
panelview
);
panelview.removeEventListener("ViewShowing", this);
+ panelview.addEventListener("command", this);
},
onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) {
this._panelMenuView.uninit();
delete this._panelMenuView;
- aEvent.target.removeEventListener("ViewHiding", this);
+ let panelview = aEvent.target;
+ panelview.removeEventListener("ViewHiding", this);
+ panelview.removeEventListener("command", this);
},
handlePlacesEvents(aEvents) {
@@ -2155,7 +2164,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..f77ce1661e 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);"/>
@@ -91,8 +91,8 @@
<command id="Browser:NewUserContextTab" oncommand="openNewUserContextTab(event.sourceEvent);"/>
<command id="Browser:OpenAboutContainers" oncommand="openPreferences('paneContainers');"/>
<command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/>
- <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/>
- <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/>
+ <command id="Tools:Downloads" oncommand="BrowserCommands.downloadsUI();"/>
+ <command id="Tools:Addons" oncommand="BrowserAddonUI.openAddonsMgr();"/>
<command id="Tools:Sanitize" oncommand="Sanitizer.showUI(window);"/>
<command id="Tools:PrivateBrowsing"
oncommand="OpenBrowserWindow({private: 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"/>
@@ -276,7 +276,7 @@
<key id="viewBookmarksSidebarKb"
data-l10n-id="bookmark-show-sidebar-shortcut"
modifiers="accel"
- oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/>
+ oncommand="SidebarController.toggle('viewBookmarksSidebar');"/>
<key id="viewBookmarksToolbarKb"
data-l10n-id="bookmark-show-toolbar-shortcut"
oncommand="BookmarkingUI.toggleBookmarksToolbar('shortcut');"
@@ -295,7 +295,7 @@
#else
modifiers="accel"
#endif
- oncommand="SidebarUI.toggle('viewHistorySidebar');"/>
+ oncommand="SidebarController.toggle('viewHistorySidebar');"/>
<key id="key_fullZoomReduce" data-l10n-id="full-zoom-reduce-shortcut" command="cmd_fullZoomReduce" modifiers="accel"/>
<key data-l10n-id="full-zoom-reduce-shortcut-alt-a" command="cmd_fullZoomReduce" modifiers="accel"/>
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
index a2a5f6ff71..eaed3950fe 100644
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -177,7 +177,6 @@ var gIdentityHandler = {
_popupInitialized: false,
_initializePopup() {
- window.ensureCustomElements("moz-support-link");
if (!this._popupInitialized) {
let wrapper = document.getElementById("template-identity-popup");
wrapper.replaceWith(wrapper.content);
@@ -456,7 +455,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 +476,7 @@ var gIdentityHandler = {
"mixed-content"
);
if (reload) {
- BrowserReload();
+ BrowserCommands.reload();
}
if (this._popupInitialized) {
PanelMultiView.hidePopup(this._identityPopup);
@@ -496,7 +497,7 @@ var gIdentityHandler = {
port,
gBrowser.contentPrincipal.originAttributes
);
- BrowserReloadSkipCache();
+ BrowserCommands.reloadSkipCache();
if (this._popupInitialized) {
PanelMultiView.hidePopup(this._identityPopup);
}
@@ -611,7 +612,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 +1261,7 @@ var gIdentityHandler = {
}
},
- handleEvent(event) {
+ handleEvent() {
let elem = document.activeElement;
let position = elem.compareDocumentPosition(this._identityPopup);
@@ -1278,7 +1279,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..de7b2cc39a 100644
--- a/browser/base/content/browser-sitePermissionPanel.js
+++ b/browser/base/content/browser-sitePermissionPanel.js
@@ -14,9 +14,6 @@ var gPermissionPanel = {
if (!this._popupInitialized) {
let wrapper = document.getElementById("template-permission-popup");
wrapper.replaceWith(wrapper.content);
-
- window.ensureCustomElements("moz-support-link");
-
this._popupInitialized = true;
}
},
@@ -353,7 +350,7 @@ var gPermissionPanel = {
}
},
- handleEvent(event) {
+ handleEvent() {
let elem = document.activeElement;
let position = elem.compareDocumentPosition(this._permissionPopup);
@@ -371,7 +368,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..31b87ac7e0 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",
},
{
@@ -1381,7 +1377,6 @@ var gProtectionsHandler = {
let wrapper = document.getElementById("template-protections-popup");
this._protectionsPopup = wrapper.content.firstElementChild;
wrapper.replaceWith(wrapper.content);
- window.ensureCustomElements("moz-support-link");
this.maybeSetMilestoneCounterText();
@@ -1595,8 +1590,6 @@ var gProtectionsHandler = {
// Add an observer to observe that the history has been cleared.
Services.obs.addObserver(this, "browser:purge-session-history");
-
- window.ensureCustomElements("moz-button-group", "moz-toggle");
},
uninit() {
@@ -1641,42 +1634,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 +2048,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 +2187,7 @@ var gProtectionsHandler = {
ContentBlockingAllowList.add(gBrowser.selectedBrowser);
if (shouldReload) {
this._hidePopup();
- BrowserReload();
+ BrowserCommands.reload();
}
},
@@ -2202,11 +2195,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 +2526,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");
@@ -2605,14 +2598,14 @@ var gProtectionsHandler = {
_createHeroElement(doc, message) {
const messageEl = this._createElement(doc, "div");
messageEl.setAttribute("id", "protections-popup-message");
- messageEl.classList.add("whatsNew-hero-message");
+ messageEl.classList.add("protections-hero-message");
const wrapperEl = this._createElement(doc, "div");
- wrapperEl.classList.add("whatsNew-message-body");
+ wrapperEl.classList.add("protections-popup-message-body");
messageEl.appendChild(wrapperEl);
wrapperEl.appendChild(
this._createElement(doc, "h2", {
- classList: "whatsNew-message-title",
+ classList: "protections-popup-message-title",
content: message.content.title,
})
);
diff --git a/browser/base/content/browser-sync.js b/browser/base/content/browser-sync.js
index a94bf2b896..9aa6cc5cd4 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();
}
@@ -202,10 +202,7 @@ this.SyncedTabsPanelList = class SyncedTabsPanelList {
}
_appendSyncClient(client, container, labelId, paginationInfo) {
- let {
- maxTabs = SyncedTabsPanelList.sRemoteTabsPerPage,
- showInactive = false,
- } = paginationInfo;
+ let { maxTabs = SyncedTabsPanelList.sRemoteTabsPerPage } = paginationInfo;
// Create the element for the remote client.
let clientItem = document.createXULElement("label");
clientItem.setAttribute("id", labelId);
@@ -227,11 +224,24 @@ this.SyncedTabsPanelList = class SyncedTabsPanelList {
);
label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
} else {
- let tabs = client.tabs.filter(t => showInactive || !t.inactive);
- let numInactive = client.tabs.length - tabs.length;
+ // We have the client obj but we need the FxA device obj so we use the clients
+ // engine to get us the FxA device
+ let device =
+ fxAccounts.device.recentDeviceList &&
+ fxAccounts.device.recentDeviceList.find(
+ d =>
+ d.id === Weave.Service.clientsEngine.getClientFxaDeviceId(client.id)
+ );
+ let remoteTabCloseAvailable =
+ device && fxAccounts.commands.closeTab.isDeviceCompatible(device);
+
+ let tabs = client.tabs.filter(t => !t.inactive);
+ let hasInactive = tabs.length != client.tabs.length;
- // If this page will display all tabs, show no additional buttons.
- // Otherwise, show a "Show More" button
+ if (hasInactive) {
+ container.append(this._createShowInactiveTabsElement(client, device));
+ }
+ // If this page isn't displaying all (regular, active) tabs, show a "Show More" button.
let hasNextPage = tabs.length > maxTabs;
let nextPageIsLastPage =
hasNextPage &&
@@ -248,15 +258,13 @@ this.SyncedTabsPanelList = class SyncedTabsPanelList {
tabs = tabs.slice(0, maxTabs);
}
for (let [index, tab] of tabs.entries()) {
- let tabEnt = this._createSyncedTabElement(tab, index);
- container.appendChild(tabEnt);
- }
- if (numInactive) {
- let elt = this._createShowInactiveTabsElement(
- paginationInfo,
- numInactive
+ let tabEnt = this._createSyncedTabElement(
+ tab,
+ index,
+ device,
+ remoteTabCloseAvailable
);
- container.appendChild(elt);
+ container.appendChild(tabEnt);
}
if (hasNextPage) {
let showAllEnt = this._createShowMoreSyncedTabsElement(paginationInfo);
@@ -265,7 +273,10 @@ this.SyncedTabsPanelList = class SyncedTabsPanelList {
}
}
- _createSyncedTabElement(tabInfo, index) {
+ _createSyncedTabElement(tabInfo, index, device, canCloseTabs) {
+ let tabContainer = document.createXULElement("hbox");
+ tabContainer.setAttribute("class", "PanelUI-tabitem-container");
+
let item = document.createXULElement("toolbarbutton");
let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
item.setAttribute("itemtype", "tab");
@@ -296,25 +307,29 @@ this.SyncedTabsPanelList = class SyncedTabsPanelList {
{}
),
});
- if (document.defaultView.whereToOpenLink(e) != "current") {
+ if (BrowserUtils.whereToOpenLink(e) != "current") {
e.preventDefault();
e.stopPropagation();
} else {
CustomizableUI.hidePanelForNode(item);
}
});
- return item;
+ tabContainer.appendChild(item);
+ // We should only add an X button next to tabs if the device
+ // is broadcasting that it can remotely close tabs
+ if (canCloseTabs) {
+ tabContainer.appendChild(
+ this._createCloseTabElement(tabInfo.url, device)
+ );
+ }
+ return tabContainer;
}
_createShowMoreSyncedTabsElement(paginationInfo) {
let showMoreItem = document.createXULElement("toolbarbutton");
showMoreItem.setAttribute("itemtype", "showmorebutton");
showMoreItem.setAttribute("closemenu", "none");
- showMoreItem.classList.add(
- "subviewbutton",
- "subviewbutton-nav",
- "subviewbutton-nav-down"
- );
+ showMoreItem.classList.add("subviewbutton", "subviewbutton-nav-down");
document.l10n.setAttributes(showMoreItem, "appmenu-remote-tabs-showmore");
paginationInfo.maxTabs = Infinity;
@@ -326,27 +341,56 @@ this.SyncedTabsPanelList = class SyncedTabsPanelList {
return showMoreItem;
}
- _createShowInactiveTabsElement(paginationInfo, count) {
+ _createShowInactiveTabsElement(client, device) {
let showItem = document.createXULElement("toolbarbutton");
- showItem.setAttribute("itemtype", "showmorebutton");
showItem.setAttribute("closemenu", "none");
- showItem.classList.add(
- "subviewbutton",
- "subviewbutton-nav",
- "subviewbutton-nav-down"
+ showItem.classList.add("subviewbutton", "subviewbutton-nav");
+ document.l10n.setAttributes(
+ showItem,
+ "appmenu-remote-tabs-show-inactive-tabs"
);
- document.l10n.setAttributes(showItem, "appmenu-remote-tabs-showinactive");
- document.l10n.setArgs(showItem, { count });
- paginationInfo.showInactive = true;
+ let canClose =
+ device && fxAccounts.commands.closeTab.isDeviceCompatible(device);
+
showItem.addEventListener("click", e => {
- e.preventDefault();
- e.stopPropagation();
- this._showSyncedTabs(paginationInfo);
+ let node = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-inactive-tabs"
+ );
+
+ // device name.
+ let label = node.querySelector("label[itemtype='client']");
+ label.textContent = client.name;
+
+ // Update the tab list.
+ let container = node.querySelector(".panel-subview-body");
+ container.replaceChildren(
+ ...client.tabs
+ .filter(t => t.inactive)
+ .map((tab, index) =>
+ this._createSyncedTabElement(tab, index, device, canClose)
+ )
+ );
+ PanelUI.showSubView("PanelUI-fxa-menu-inactive-tabs", showItem, e);
});
return showItem;
}
+ _createCloseTabElement(url, device) {
+ let closeBtn = document.createXULElement("image");
+ closeBtn.setAttribute("class", "close-icon remotetabs-close");
+
+ closeBtn.addEventListener("click", function (e) {
+ e.stopPropagation();
+ // The user could be hitting multiple tabs across multiple devices, with a few
+ // seconds in-between -- we should not immediately fire off pushes, so we
+ // add it to a queue and send in bulk at a later time
+ fxAccounts.commands.closeTab.enqueueTabToClose(device, url);
+ });
+ return closeBtn;
+ }
+
destroy() {
Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
this.tabsList = null;
@@ -384,7 +428,7 @@ var gSync = {
"browser/accounts.ftl",
"browser/appmenu.ftl",
"browser/sync.ftl",
- "toolkit/branding/accounts.ftl",
+ "browser/syncedTabs.ftl",
],
true
));
@@ -445,7 +489,7 @@ var gSync = {
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
- "PXI_TOOLBAR_ENABLED",
+ "FXA_CTA_MENU_ENABLED",
"identity.fxaccounts.toolbar.pxiToolbarEnabled"
);
},
@@ -533,10 +577,23 @@ var gSync = {
let fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa");
fxaPanelView.addEventListener("ViewShowing", this);
fxaPanelView.addEventListener("ViewHiding", this);
+ fxaPanelView.addEventListener("command", this);
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-syncnow-button"
+ ).addEventListener("mouseover", this);
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sendtab-not-configured-button"
+ ).addEventListener("command", this);
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sendtab-connect-device-button"
+ ).addEventListener("command", this);
// If the experiment is enabled, we'll need to update the panels
// to show some different text to the user
- if (this.PXI_TOOLBAR_ENABLED) {
+ if (this.FXA_CTA_MENU_ENABLED) {
this.updateFxAPanel(UIState.get());
this.updateCTAPanel();
}
@@ -558,6 +615,13 @@ var gSync = {
handleEvent(event) {
switch (event.type) {
+ case "mouseover":
+ this.refreshSyncButtonsTooltip();
+ break;
+ case "command": {
+ this.onCommand(event.target);
+ break;
+ }
case "ViewShowing": {
this.onFxAPanelViewShowing(event.target);
break;
@@ -606,16 +670,61 @@ var gSync = {
panelview.syncedTabsPanelList = null;
},
+ onCommand(button) {
+ switch (button.id) {
+ case "PanelUI-fxa-menu-sync-prefs-button":
+ // fall through
+ case "PanelUI-fxa-menu-setup-sync-button":
+ this.openPrefsFromFxaMenu("sync_settings", button);
+ break;
+
+ case "PanelUI-fxa-menu-sendtab-connect-device-button":
+ // fall through
+ case "PanelUI-fxa-menu-connect-device-button":
+ this.openConnectAnotherDeviceFromFxaMenu(button);
+ break;
+
+ case "fxa-manage-account-button":
+ this.clickFxAMenuHeaderButton(button);
+ break;
+ case "PanelUI-fxa-menu-syncnow-button":
+ this.doSyncFromFxaMenu(button);
+ break;
+ case "PanelUI-fxa-menu-sendtab-button":
+ this.showSendToDeviceViewFromFxaMenu(button);
+ break;
+ case "PanelUI-fxa-menu-account-signout-button":
+ this.disconnect();
+ break;
+ case "PanelUI-fxa-menu-sync-button":
+ this.openPrefsFromFxaButton("sync_cta", button);
+ break;
+ case "PanelUI-fxa-menu-monitor-button":
+ this.openMonitorLink(button);
+ break;
+ case "PanelUI-fxa-menu-relay-button":
+ this.openRelayLink(button);
+ break;
+ case "PanelUI-fxa-menu-vpn-button":
+ this.openVPNLink(button);
+ break;
+ case "PanelUI-fxa-menu-sendtab-not-configured-button":
+ this.openPrefsFromFxaMenu("send_tab", button);
+ break;
+ }
+ },
+
observe(subject, topic, data) {
if (!this._initialized) {
console.error("browser-sync observer called after unload: ", topic);
return;
}
switch (topic) {
- case UIState.ON_UPDATE:
+ case UIState.ON_UPDATE: {
const state = UIState.get();
this.updateAllUI(state);
break;
+ }
case "quit-application":
// Stop the animation timer on shutdown, since we can't update the UI
// after this.
@@ -637,7 +746,6 @@ var gSync = {
this.updateSyncButtonsTooltip(state);
this.updateSyncStatus(state);
this.updateFxAPanel(state);
- this.updateCTAPanel(state);
// Ensure we have something in the device list in the background.
this.ensureFxaDevices();
},
@@ -648,7 +756,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;
@@ -720,16 +828,6 @@ var gSync = {
this.emitFxaToolbarTelemetry("send_tab", anchor);
},
- showRemoteTabsFromFxaMenu(panel) {
- PanelUI.showSubView("PanelUI-remotetabs", panel);
- this.emitFxaToolbarTelemetry("sync_tabs", panel);
- },
-
- showSidebarFromFxaMenu(panel) {
- SidebarUI.toggle("viewTabsSidebar");
- this.emitFxaToolbarTelemetry("sync_tabs_sidebar", panel);
- },
-
_populateSendTabToDevicesView(panelViewNode, reloadDevices = true) {
let bodyNode = panelViewNode.querySelector(".panel-subview-body");
let panelNode = panelViewNode.closest("panel");
@@ -768,7 +866,7 @@ var gSync = {
}
}
- item.addEventListener("command", event => {
+ item.addEventListener("command", () => {
if (panelNode) {
PanelMultiView.hidePopup(panelNode);
}
@@ -830,12 +928,20 @@ var gSync = {
let fxaStatus = document.documentElement.getAttribute("fxastatus");
if (fxaStatus == "not_configured") {
+ // sign in button in app (hamburger) menu
+ // should take you straight to fxa sign in page
+ if (anchor.id == "appMenu-fxa-label2") {
+ this.openFxAEmailFirstPageFromFxaMenu(anchor);
+ PanelUI.hide();
+ return;
+ }
+
// If we're signed out but have the PXI pref enabled
// we should show the PXI panel instead of taking the user
// straight to FxA sign-in
- if (this.PXI_TOOLBAR_ENABLED) {
+ if (this.FXA_CTA_MENU_ENABLED) {
this.updateFxAPanel(UIState.get());
- this.updateCTAPanel();
+ this.updateCTAPanel(anchor);
PanelUI.showSubView("PanelUI-fxa", anchor, aEvent);
} else if (anchor == document.getElementById("fxa-toolbar-menu-button")) {
// The fxa toolbar button doesn't have much context before the user
@@ -844,20 +950,13 @@ var gSync = {
this.emitFxaToolbarTelemetry("toolbar_icon", anchor);
openTrustedLinkIn("about:preferences#sync", "tab");
PanelUI.hide();
- } else {
- let panel =
- anchor.id == "appMenu-fxa-label2"
- ? PanelMultiView.getViewNode(document, "PanelUI-fxa")
- : undefined;
- this.openFxAEmailFirstPageFromFxaMenu(panel);
- PanelUI.hide();
}
return;
}
// If the user is signed in and we have the PXI pref enabled then add
// the pxi panel to the existing toolbar
- if (this.PXI_TOOLBAR_ENABLED) {
- this.updateCTAPanel();
+ if (this.FXA_CTA_MENU_ENABLED) {
+ this.updateCTAPanel(anchor);
}
if (!gFxaToolbarAccessed) {
@@ -932,21 +1031,16 @@ var gSync = {
fxaMenuAccountButtonEl.removeAttribute("closemenu");
syncSetupButtonEl.removeAttribute("hidden");
- let headerTitleL10nId = this.PXI_TOOLBAR_ENABLED
- ? "appmenuitem-sign-in-account"
- : "appmenuitem-fxa-sign-in";
+ let headerTitleL10nId = this.FXA_CTA_MENU_ENABLED
+ ? "synced-tabs-fxa-sign-in"
+ : "appmenuitem-sign-in-account";
let headerDescription;
if (state.status === UIState.STATUS_NOT_CONFIGURED) {
mainWindowEl.style.removeProperty("--avatar-image-url");
- headerDescription = this.fluentStrings.formatValueSync(
- "appmenu-fxa-signed-in-label"
- );
- // Signed out, expeirment enabled is the only state we want to hide the
- // header description, so we make it empty and check for that when setting
- // the value
- if (this.PXI_TOOLBAR_ENABLED) {
- headerDescription = "";
- }
+ const headerDescString = this.FXA_CTA_MENU_ENABLED
+ ? "fxa-menu-sync-description"
+ : "appmenu-fxa-signed-in-label";
+ headerDescription = this.fluentStrings.formatValueSync(headerDescString);
} else if (state.status === UIState.STATUS_LOGIN_FAILED) {
stateValue = "login-failed";
headerTitleL10nId = "account-disconnected2";
@@ -1020,8 +1114,8 @@ var gSync = {
).hidden = !canSendAllURIs;
},
- emitFxaToolbarTelemetry(type, panel) {
- if (UIState.isReady() && panel) {
+ emitFxaToolbarTelemetry(type, sourceElement) {
+ if (UIState.isReady() && sourceElement) {
const state = UIState.get();
const hasAvatar = state.avatarURL && !state.avatarIsDefault;
let extraOptions = {
@@ -1029,10 +1123,10 @@ var gSync = {
fxa_avatar: hasAvatar ? "true" : "false",
};
- // When the fxa avatar panel is within the Firefox app menu,
+ // When the source element is within the Firefox app menu,
// we emit different telemetry.
let eventName = "fxa_avatar_menu";
- if (this.isPanelInsideAppMenu(panel)) {
+ if (this.isInsideAppMenu(sourceElement)) {
eventName = "fxa_app_menu";
}
@@ -1046,9 +1140,9 @@ var gSync = {
}
},
- isPanelInsideAppMenu(panel = undefined) {
+ isInsideAppMenu(sourceElement = undefined) {
const appMenuPanel = document.getElementById("appMenu-popup");
- if (panel && appMenuPanel.contains(panel)) {
+ if (sourceElement && appMenuPanel.contains(sourceElement)) {
return true;
}
return false;
@@ -1225,10 +1319,10 @@ var gSync = {
openTrustedLinkIn(url, "tab");
},
- async openConnectAnotherDeviceFromFxaMenu(panel = undefined) {
- this.emitFxaToolbarTelemetry("cad", panel);
+ async openConnectAnotherDeviceFromFxaMenu(sourceElement = undefined) {
+ this.emitFxaToolbarTelemetry("cad", sourceElement);
let entryPoint = "fxa_discoverability_native";
- if (this.isPanelInsideAppMenu(panel)) {
+ if (this.isInsideAppMenu(sourceElement)) {
entryPoint = "fxa_app_menu";
}
this.openConnectAnotherDevice(entryPoint);
@@ -1241,7 +1335,7 @@ var gSync = {
switchToTabHavingURI(url, true, { replaceQueryString: true });
},
- async clickFxAMenuHeaderButton(panel = undefined) {
+ async clickFxAMenuHeaderButton(sourceElement = undefined) {
// Depending on the current logged in state of a user,
// clicking the FxA header will either open
// a sign-in page, account management page, or sync
@@ -1249,16 +1343,16 @@ var gSync = {
const { status } = UIState.get();
switch (status) {
case UIState.STATUS_NOT_CONFIGURED:
- this.openFxAEmailFirstPageFromFxaMenu(panel);
+ this.openFxAEmailFirstPageFromFxaMenu(sourceElement);
break;
case UIState.STATUS_LOGIN_FAILED:
- this.openPrefsFromFxaMenu("sync_settings", panel);
+ this.openPrefsFromFxaMenu("sync_settings", sourceElement);
break;
case UIState.STATUS_NOT_VERIFIED:
this.openFxAEmailFirstPage("fxa_app_menu_reverify");
break;
case UIState.STATUS_SIGNED_IN:
- this.openFxAManagePageFromFxaMenu(panel);
+ this.openFxAManagePageFromFxaMenu(sourceElement);
}
},
@@ -1273,10 +1367,13 @@ var gSync = {
switchToTabHavingURI(url, true, { replaceQueryString: true });
},
- async openFxAEmailFirstPageFromFxaMenu(panel = undefined, extraParams = {}) {
- this.emitFxaToolbarTelemetry("login", panel);
+ async openFxAEmailFirstPageFromFxaMenu(
+ sourceElement = undefined,
+ extraParams = {}
+ ) {
+ this.emitFxaToolbarTelemetry("login", sourceElement);
let entryPoint = "fxa_discoverability_native";
- if (panel) {
+ if (sourceElement) {
entryPoint = "fxa_toolbar_button";
}
this.openFxAEmailFirstPage(entryPoint, extraParams);
@@ -1287,10 +1384,10 @@ var gSync = {
switchToTabHavingURI(url, true, { replaceQueryString: true });
},
- async openFxAManagePageFromFxaMenu(panel = undefined) {
- this.emitFxaToolbarTelemetry("account_settings", panel);
+ async openFxAManagePageFromFxaMenu(sourceElement = undefined) {
+ this.emitFxaToolbarTelemetry("account_settings", sourceElement);
let entryPoint = "fxa_discoverability_native";
- if (this.isPanelInsideAppMenu(panel)) {
+ if (this.isInsideAppMenu(sourceElement)) {
entryPoint = "fxa_app_menu";
}
this.openFxAManagePage(entryPoint);
@@ -1364,7 +1461,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 +1559,7 @@ var gSync = {
fxAccounts.flushLogFile();
});
};
- const onSendAllCommand = event => {
+ const onSendAllCommand = () => {
send(targets);
};
const onTargetDeviceCommand = event => {
@@ -1893,9 +1990,9 @@ var gSync = {
}
},
- doSyncFromFxaMenu(panel) {
+ doSyncFromFxaMenu(sourceElement) {
this.doSync();
- this.emitFxaToolbarTelemetry("sync_now", panel);
+ this.emitFxaToolbarTelemetry("sync_now", sourceElement);
},
openPrefs(entryPoint = "syncbutton", origin = undefined) {
@@ -1905,18 +2002,18 @@ var gSync = {
});
},
- openPrefsFromFxaMenu(type, panel) {
- this.emitFxaToolbarTelemetry(type, panel);
+ openPrefsFromFxaMenu(type, sourceElement) {
+ this.emitFxaToolbarTelemetry(type, sourceElement);
let entryPoint = "fxa_discoverability_native";
- if (this.isPanelInsideAppMenu(panel)) {
+ if (this.isInsideAppMenu(sourceElement)) {
entryPoint = "fxa_app_menu";
}
this.openPrefs(entryPoint);
},
- openPrefsFromFxaButton(type, panel) {
+ openPrefsFromFxaButton(type, sourceElement) {
let entryPoint = "fxa_toolbar_button_sync";
- this.emitFxaToolbarTelemetry(type, panel);
+ this.emitFxaToolbarTelemetry(type, sourceElement);
this.openPrefs(entryPoint);
},
@@ -2049,27 +2146,24 @@ var gSync = {
// This should only be shown if we have enabled the pxiPanel via
// an experiment or explicitly through prefs
- updateCTAPanel() {
+ updateCTAPanel(anchor) {
const mainPanelEl = PanelMultiView.getViewNode(
document,
"PanelUI-fxa-cta-menu"
);
- const syncCtaEl = PanelMultiView.getViewNode(
- document,
- "PanelUI-fxa-menu-sync-button"
- );
- // If we're not in the experiment then we do not enable this at all
- if (!this.PXI_TOOLBAR_ENABLED) {
+ // If we're not in the experiment or in the app menu (hamburger)
+ // do not show this CTA panel
+ if (
+ !this.FXA_CTA_MENU_ENABLED ||
+ (anchor && anchor.id === "appMenu-fxa-label2")
+ ) {
// If we've previously shown this but got disabled
// we should ensure we hide the panel
mainPanelEl.hidden = true;
return;
}
- // If we're already signed in an syncing, we shouldn't show the sync CTA
- syncCtaEl.hidden = this.isSignedIn;
-
// Monitor checks
let monitorPanelEl = PanelMultiView.getViewNode(
document,
@@ -2112,8 +2206,8 @@ var gSync = {
!monitorEnabled && !relayEnabled && !vpnEnabled;
mainPanelEl.hidden = false;
},
- async openMonitorLink(panel) {
- this.emitFxaToolbarTelemetry("monitor_cta", panel);
+ async openMonitorLink(sourceElement) {
+ this.emitFxaToolbarTelemetry("monitor_cta", sourceElement);
await this.openCtaLink(
FX_MONITOR_OAUTH_CLIENT_ID,
new URL("https://monitor.firefox.com"),
@@ -2121,8 +2215,8 @@ var gSync = {
);
},
- async openRelayLink(panel) {
- this.emitFxaToolbarTelemetry("relay_cta", panel);
+ async openRelayLink(sourceElement) {
+ this.emitFxaToolbarTelemetry("relay_cta", sourceElement);
await this.openCtaLink(
FX_RELAY_OAUTH_CLIENT_ID,
new URL("https://relay.firefox.com"),
@@ -2130,8 +2224,8 @@ var gSync = {
);
},
- async openVPNLink(panel) {
- this.emitFxaToolbarTelemetry("vpn_cta", panel);
+ async openVPNLink(sourceElement) {
+ this.emitFxaToolbarTelemetry("vpn_cta", sourceElement);
await this.openCtaLink(
VPN_OAUTH_CLIENT_ID,
new URL("https://www.mozilla.org/en-US/products/vpn/"),
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..5f41ca7781 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -9,6 +9,9 @@ var { XPCOMUtils } = ChromeUtils.importESModule(
var { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
+ChromeUtils.importESModule(
+ "resource://gre/modules/MemoryNotificationDB.sys.mjs"
+);
ChromeUtils.importESModule("resource://gre/modules/NotificationDB.sys.mjs");
// lazy module getters
@@ -35,8 +38,6 @@ ChromeUtils.defineESModuleGetters(this, {
DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
ExtensionsUI: "resource:///modules/ExtensionsUI.sys.mjs",
- FirefoxViewNotificationManager:
- "resource:///modules/firefox-view-notification-manager.sys.mjs",
HomePage: "resource:///modules/HomePage.sys.mjs",
isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
LightweightThemeConsumer:
@@ -79,7 +80,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 +109,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 +552,7 @@ XPCOMUtils.defineLazyPreferenceGetter(
"gFxaToolbarAccessed",
"identity.fxaccounts.toolbar.accessed",
false,
- (aPref, aOldVal, aNewVal) => {
+ () => {
updateFxaToolbarMenu(gFxaToolbarEnabled);
}
);
@@ -691,7 +697,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 +1024,7 @@ const gClickAndHoldListenersOnElement = {
};
const gSessionHistoryObserver = {
- observe(subject, topic, data) {
+ observe(subject, topic) {
if (topic != "browser:purge-session-history") {
return;
}
@@ -1037,7 +1042,7 @@ const gSessionHistoryObserver = {
const gStoragePressureObserver = {
_lastNotificationTime: -1,
- async observe(subject, topic, data) {
+ async observe(subject, topic) {
if (topic != "QuotaManager::StoragePressure") {
return;
}
@@ -1088,7 +1093,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 +1480,7 @@ var gKeywordURIFixup = {
);
},
- observe(fixupInfo, topic, data) {
+ observe(fixupInfo) {
fixupInfo.QueryInterface(Ci.nsIURIFixupInfo);
let browser = fixupInfo.consumer?.top?.embedderElement;
@@ -1499,1154 +1504,36 @@ function _createNullPrincipalFromTabUserContextId(tab = gBrowser.selectedTab) {
});
}
-let _resolveDelayedStartup;
-var delayedStartupPromise = new Promise(resolve => {
- _resolveDelayedStartup = resolve;
-});
-
-var gBrowserInit = {
- delayedStartupFinished: false,
- idleTasksFinishedPromise: null,
- idleTaskPromiseResolve: null,
- domContentLoaded: false,
-
- _tabToAdopt: undefined,
-
- _setupFirstContentWindowPaintPromise() {
- let lastTransactionId = window.windowUtils.lastTransactionId;
- let layerTreeListener = () => {
- if (this.getTabToAdopt()) {
- // Need to wait until we finish adopting the tab, or we might end
- // up focusing the initial browser and then losing focus when it
- // gets swapped out for the tab to adopt.
- return;
- }
- removeEventListener("MozLayerTreeReady", layerTreeListener);
- let listener = e => {
- if (e.transactionId > lastTransactionId) {
- window.removeEventListener("MozAfterPaint", listener);
- this._firstContentWindowPaintDeferred.resolve();
- }
- };
- addEventListener("MozAfterPaint", listener);
- };
- addEventListener("MozLayerTreeReady", layerTreeListener);
- },
-
- getTabToAdopt() {
- if (this._tabToAdopt !== undefined) {
- return this._tabToAdopt;
- }
-
- if (window.arguments && window.XULElement.isInstance(window.arguments[0])) {
- this._tabToAdopt = window.arguments[0];
-
- // Clear the reference of the tab being adopted from the arguments.
- window.arguments[0] = null;
- } else {
- // There was no tab to adopt in the arguments, set _tabToAdopt to null
- // to avoid checking it again.
- this._tabToAdopt = null;
- }
-
- return this._tabToAdopt;
- },
-
- _clearTabToAdopt() {
- this._tabToAdopt = null;
- },
-
- // Used to check if the new window is still adopting an existing tab as its first tab
- // (e.g. from the WebExtensions internals).
- isAdoptingTab() {
- return !!this.getTabToAdopt();
- },
-
- onBeforeInitialXULLayout() {
- this._setupFirstContentWindowPaintPromise();
-
- updateBookmarkToolbarVisibility();
-
- // Set a sane starting width/height for all resolutions on new profiles.
- if (ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null)) {
- // When the fingerprinting resistance is enabled, making sure that we don't
- // have a maximum window to interfere with generating rounded window dimensions.
- document.documentElement.setAttribute("sizemode", "normal");
- } else if (!document.documentElement.hasAttribute("width")) {
- const TARGET_WIDTH = 1280;
- const TARGET_HEIGHT = 1040;
- let width = Math.min(screen.availWidth * 0.9, TARGET_WIDTH);
- let height = Math.min(screen.availHeight * 0.9, TARGET_HEIGHT);
-
- document.documentElement.setAttribute("width", width);
- document.documentElement.setAttribute("height", height);
-
- if (width < TARGET_WIDTH && height < TARGET_HEIGHT) {
- document.documentElement.setAttribute("sizemode", "maximized");
- }
- }
- if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
- const toolbarMenubar = document.getElementById("toolbar-menubar");
- // set a default value
- if (!toolbarMenubar.hasAttribute("autohide")) {
- toolbarMenubar.setAttribute("autohide", true);
- }
- document.l10n.setAttributes(
- toolbarMenubar,
- "toolbar-context-menu-menu-bar-cmd"
- );
- toolbarMenubar.setAttribute("data-l10n-attrs", "toolbarname");
- }
-
- // Run menubar initialization first, to avoid TabsInTitlebar code picking
- // up mutations from it and causing a reflow.
- AutoHideMenubar.init();
- // Update the chromemargin attribute so the window can be sized correctly.
- window.TabBarVisibility.update();
- TabsInTitlebar.init();
-
- new LightweightThemeConsumer(document);
-
- if (
- Services.prefs.getBoolPref(
- "toolkit.legacyUserProfileCustomizations.windowIcon",
- false
- )
- ) {
- document.documentElement.setAttribute("icon", "main-window");
- }
-
- // Call this after we set attributes that might change toolbars' computed
- // text color.
- ToolbarIconColor.init();
- },
-
- onDOMContentLoaded() {
- // This needs setting up before we create the first remote browser.
- window.docShell.treeOwner
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIAppWindow).XULBrowserWindow = window.XULBrowserWindow;
- window.browserDOMWindow = new nsBrowserAccess();
-
- gBrowser = window._gBrowser;
- delete window._gBrowser;
- gBrowser.init();
-
- BrowserWindowTracker.track(window);
-
- FirefoxViewHandler.init();
-
- gNavToolbox.palette = document.getElementById(
- "BrowserToolbarPalette"
- ).content;
- for (let area of CustomizableUI.areas) {
- let type = CustomizableUI.getAreaType(area);
- if (type == CustomizableUI.TYPE_TOOLBAR) {
- let node = document.getElementById(area);
- CustomizableUI.registerToolbarNode(node);
- }
- }
- BrowserSearch.initPlaceHolder();
-
- // Hack to ensure that the various initial pages favicon is loaded
- // instantaneously, to avoid flickering and improve perceived performance.
- this._callWithURIToLoad(uriToLoad => {
- let url;
- try {
- url = Services.io.newURI(uriToLoad);
- } catch (e) {
- return;
- }
- let nonQuery = url.prePath + url.filePath;
- if (nonQuery in gPageIcons) {
- gBrowser.setIcon(gBrowser.selectedTab, gPageIcons[nonQuery]);
- }
- });
-
- updateFxaToolbarMenu(gFxaToolbarEnabled, true);
-
- updatePrintCommands(gPrintEnabled);
-
- gUnifiedExtensions.init();
-
- // Setting the focus will cause a style flush, it's preferable to call anything
- // that will modify the DOM from within this function before this call.
- this._setInitialFocus();
-
- this.domContentLoaded = true;
- },
-
- onLoad() {
- gBrowser.addEventListener("DOMUpdateBlockedPopups", gPopupBlockerObserver);
- gBrowser.addEventListener(
- "TranslationsParent:LanguageState",
- FullPageTranslationsPanel
- );
- gBrowser.addEventListener(
- "TranslationsParent:OfferTranslation",
- FullPageTranslationsPanel
- );
- gBrowser.addTabsProgressListener(FullPageTranslationsPanel);
-
- window.addEventListener("AppCommand", HandleAppCommandEvent, true);
-
- // These routines add message listeners. They must run before
- // loading the frame script to ensure that we don't miss any
- // message sent between when the frame script is loaded and when
- // the listener is registered.
- CaptivePortalWatcher.init();
- ZoomUI.init(window);
-
- if (!gMultiProcessBrowser) {
- // There is a Content:Click message manually sent from content.
- gBrowser.tabpanels.addEventListener("click", contentAreaClick, {
- capture: true,
- mozSystemGroup: true,
- });
- }
-
- // hook up UI through progress listener
- gBrowser.addProgressListener(window.XULBrowserWindow);
- gBrowser.addTabsProgressListener(window.TabsProgressListener);
-
- SidebarUI.init();
-
- // We do this in onload because we want to ensure the button's state
- // doesn't flicker as the window is being shown.
- DownloadsButton.init();
-
- // Certain kinds of automigration rely on this notification to complete
- // their tasks BEFORE the browser window is shown. SessionStore uses it to
- // restore tabs into windows AFTER important parts like gMultiProcessBrowser
- // have been initialized.
- Services.obs.notifyObservers(window, "browser-window-before-show");
-
- if (!window.toolbar.visible) {
- // adjust browser UI for popups
- gURLBar.readOnly = true;
- }
-
- // Misc. inits.
- gUIDensity.init();
- TabletModeUpdater.init();
- CombinedStopReload.ensureInitialized();
- gPrivateBrowsingUI.init();
- BrowserSearch.init();
- BrowserPageActions.init();
- if (gToolbarKeyNavEnabled) {
- ToolbarKeyboardNavigator.init();
- }
-
- // Update UI if browser is under remote control.
- gRemoteControl.updateVisualCue();
-
- // If we are given a tab to swap in, take care of it before first paint to
- // avoid an about:blank flash.
- let tabToAdopt = this.getTabToAdopt();
- if (tabToAdopt) {
- let evt = new CustomEvent("before-initial-tab-adopted", {
- bubbles: true,
- });
- gBrowser.tabpanels.dispatchEvent(evt);
-
- // Stop the about:blank load
- gBrowser.stop();
-
- // Remove the speculative focus from the urlbar to let the url be formatted.
- gURLBar.removeAttribute("focused");
-
- let swapBrowsers = () => {
- try {
- gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, tabToAdopt);
- } catch (e) {
- console.error(e);
- }
-
- // Clear the reference to the tab once its adoption has been completed.
- this._clearTabToAdopt();
- };
- if (tabToAdopt.linkedBrowser.isRemoteBrowser) {
- // For remote browsers, wait for the paint event, otherwise the tabs
- // are not yet ready and focus gets confused because the browser swaps
- // out while tabs are switching.
- addEventListener("MozAfterPaint", swapBrowsers, { once: true });
- } else {
- swapBrowsers();
- }
- }
-
- // Wait until chrome is painted before executing code not critical to making the window visible
- this._boundDelayedStartup = this._delayedStartup.bind(this);
- window.addEventListener("MozAfterPaint", this._boundDelayedStartup);
-
- if (!PrivateBrowsingUtils.enabled) {
- document.getElementById("Tools:PrivateBrowsing").hidden = true;
- // Setting disabled doesn't disable the shortcut, so we just remove
- // the keybinding.
- document.getElementById("key_privatebrowsing").remove();
- }
-
- if (BrowserUIUtils.quitShortcutDisabled) {
- document.getElementById("key_quitApplication").remove();
- document.getElementById("menu_FileQuitItem").removeAttribute("key");
-
- PanelMultiView.getViewNode(
- document,
- "appMenu-quit-button2"
- )?.removeAttribute("key");
- }
-
- this._loadHandled = true;
- },
-
- _cancelDelayedStartup() {
- window.removeEventListener("MozAfterPaint", this._boundDelayedStartup);
- this._boundDelayedStartup = null;
- },
-
- _delayedStartup() {
- let { TelemetryTimestamps } = ChromeUtils.importESModule(
- "resource://gre/modules/TelemetryTimestamps.sys.mjs"
- );
- TelemetryTimestamps.add("delayedStartupStarted");
-
- this._cancelDelayedStartup();
-
- // Bug 1531854 - The hidden window is force-created here
- // until all of its dependencies are handled.
- Services.appShell.hiddenDOMWindow;
-
- gBrowser.addEventListener(
- "PermissionStateChange",
- function () {
- gIdentityHandler.refreshIdentityBlock();
- gPermissionPanel.updateSharingIndicator();
- },
- true
- );
-
- this._handleURIToLoad();
-
- Services.obs.addObserver(gIdentityHandler, "perm-changed");
- Services.obs.addObserver(gRemoteControl, "devtools-socket");
- Services.obs.addObserver(gRemoteControl, "marionette-listening");
- Services.obs.addObserver(gRemoteControl, "remote-listening");
- Services.obs.addObserver(
- gSessionHistoryObserver,
- "browser:purge-session-history"
- );
- Services.obs.addObserver(
- gStoragePressureObserver,
- "QuotaManager::StoragePressure"
- );
- Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled");
- Services.obs.addObserver(gXPInstallObserver, "addon-install-started");
- Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked");
- Services.obs.addObserver(
- gXPInstallObserver,
- "addon-install-fullscreen-blocked"
- );
- Services.obs.addObserver(
- gXPInstallObserver,
- "addon-install-origin-blocked"
- );
- Services.obs.addObserver(
- gXPInstallObserver,
- "addon-install-policy-blocked"
- );
- Services.obs.addObserver(
- gXPInstallObserver,
- "addon-install-webapi-blocked"
- );
- Services.obs.addObserver(gXPInstallObserver, "addon-install-failed");
- Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation");
- Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup");
-
- BrowserOffline.init();
- CanvasPermissionPromptHelper.init();
- WebAuthnPromptHelper.init();
- ContentAnalysis.initialize();
-
- // Initialize the full zoom setting.
- // We do this before the session restore service gets initialized so we can
- // apply full zoom settings to tabs restored by the session restore service.
- FullZoom.init();
- PanelUI.init(shouldSuppressPopupNotifications);
- ReportBrokenSite.init(gBrowser);
-
- UpdateUrlbarSearchSplitterState();
-
- BookmarkingUI.init();
- BrowserSearch.delayedStartupInit();
- SearchUIUtils.init();
- gProtectionsHandler.init();
- HomePage.delayedStartup().catch(console.error);
-
- let safeMode = document.getElementById("helpSafeMode");
- if (Services.appinfo.inSafeMode) {
- document.l10n.setAttributes(safeMode, "menu-help-exit-troubleshoot-mode");
- safeMode.setAttribute(
- "appmenu-data-l10n-id",
- "appmenu-help-exit-troubleshoot-mode"
- );
- }
-
- // BiDi UI
- gBidiUI = isBidiEnabled();
- if (gBidiUI) {
- document.getElementById("documentDirection-separator").hidden = false;
- document.getElementById("documentDirection-swap").hidden = false;
- document.getElementById("textfieldDirection-separator").hidden = false;
- document.getElementById("textfieldDirection-swap").hidden = false;
- }
-
- // Setup click-and-hold gestures access to the session history
- // menus if global click-and-hold isn't turned on
- if (!Services.prefs.getBoolPref("ui.click_hold_context_menus", false)) {
- SetClickAndHoldHandlers();
- }
-
- function initBackForwardButtonTooltip(tooltipId, l10nId, shortcutId) {
- let shortcut = document.getElementById(shortcutId);
- shortcut = ShortcutUtils.prettifyShortcut(shortcut);
-
- let tooltip = document.getElementById(tooltipId);
- document.l10n.setAttributes(tooltip, l10nId, { shortcut });
- }
-
- initBackForwardButtonTooltip(
- "back-button-tooltip-description",
- "navbar-tooltip-back-2",
- "goBackKb"
- );
-
- initBackForwardButtonTooltip(
- "forward-button-tooltip-description",
- "navbar-tooltip-forward-2",
- "goForwardKb"
- );
-
- PlacesToolbarHelper.init();
-
- ctrlTab.readPref();
- Services.prefs.addObserver(ctrlTab.prefName, ctrlTab);
-
- // The object handling the downloads indicator is initialized here in the
- // delayed startup function, but the actual indicator element is not loaded
- // unless there are downloads to be displayed.
- DownloadsButton.initializeIndicator();
-
- if (AppConstants.platform != "macosx") {
- updateEditUIVisibility();
- let placesContext = document.getElementById("placesContext");
- placesContext.addEventListener("popupshowing", updateEditUIVisibility);
- placesContext.addEventListener("popuphiding", updateEditUIVisibility);
- }
-
- FullScreen.init();
- MenuTouchModeObserver.init();
-
- if (AppConstants.MOZ_DATA_REPORTING) {
- gDataNotificationInfoBar.init();
- }
-
- if (!AppConstants.MOZILLA_OFFICIAL) {
- DevelopmentHelpers.init();
- }
-
- gExtensionsNotifications.init();
-
- let wasMinimized = window.windowState == window.STATE_MINIMIZED;
- window.addEventListener("sizemodechange", () => {
- let isMinimized = window.windowState == window.STATE_MINIMIZED;
- if (wasMinimized != isMinimized) {
- wasMinimized = isMinimized;
- UpdatePopupNotificationsVisibility();
- }
- });
-
- window.addEventListener("mousemove", MousePosTracker);
- window.addEventListener("dragover", MousePosTracker);
-
- gNavToolbox.addEventListener("customizationstarting", CustomizationHandler);
- gNavToolbox.addEventListener("aftercustomization", CustomizationHandler);
-
- SessionStore.promiseInitialized.then(() => {
- // Bail out if the window has been closed in the meantime.
- if (window.closed) {
- return;
- }
-
- // Enable the Restore Last Session command if needed
- RestoreLastSessionObserver.init();
-
- SidebarUI.startDelayedLoad();
-
- PanicButtonNotifier.init();
- });
-
- if (BrowserHandler.kiosk) {
- // We don't modify popup windows for kiosk mode
- if (!gURLBar.readOnly) {
- window.fullScreen = true;
- }
- }
-
- if (Services.policies.status === Services.policies.ACTIVE) {
- if (!Services.policies.isAllowed("hideShowMenuBar")) {
- document
- .getElementById("toolbar-menubar")
- .removeAttribute("toolbarname");
- }
- if (!Services.policies.isAllowed("filepickers")) {
- let savePageCommand = document.getElementById("Browser:SavePage");
- let openFileCommand = document.getElementById("Browser:OpenFile");
-
- savePageCommand.setAttribute("disabled", "true");
- openFileCommand.setAttribute("disabled", "true");
-
- document.addEventListener("FilePickerBlocked", function (event) {
- let browser = event.target;
-
- let notificationBox = browser
- .getTabBrowser()
- ?.getNotificationBox(browser);
-
- // Prevent duplicate notifications
- if (
- notificationBox &&
- !notificationBox.getNotificationWithValue("filepicker-blocked")
- ) {
- notificationBox.appendNotification("filepicker-blocked", {
- label: {
- "l10n-id": "filepicker-blocked-infobar",
- },
- priority: notificationBox.PRIORITY_INFO_LOW,
- });
- }
- });
- }
- let policies = Services.policies.getActivePolicies();
- if ("ManagedBookmarks" in policies) {
- let managedBookmarks = policies.ManagedBookmarks;
- let children = managedBookmarks.filter(
- child => !("toplevel_name" in child)
- );
- if (children.length) {
- let managedBookmarksButton =
- document.createXULElement("toolbarbutton");
- managedBookmarksButton.setAttribute("id", "managed-bookmarks");
- managedBookmarksButton.setAttribute("class", "bookmark-item");
- let toplevel = managedBookmarks.find(
- element => "toplevel_name" in element
- );
- if (toplevel) {
- managedBookmarksButton.setAttribute(
- "label",
- toplevel.toplevel_name
- );
- } else {
- document.l10n.setAttributes(
- managedBookmarksButton,
- "managed-bookmarks"
- );
- }
- managedBookmarksButton.setAttribute("context", "placesContext");
- managedBookmarksButton.setAttribute("container", "true");
- managedBookmarksButton.setAttribute("removable", "false");
- managedBookmarksButton.setAttribute("type", "menu");
-
- let managedBookmarksPopup = document.createXULElement("menupopup");
- managedBookmarksPopup.setAttribute("id", "managed-bookmarks-popup");
- managedBookmarksPopup.setAttribute(
- "oncommand",
- "PlacesToolbarHelper.openManagedBookmark(event);"
- );
- managedBookmarksPopup.setAttribute(
- "ondragover",
- "event.dataTransfer.effectAllowed='none';"
- );
- managedBookmarksPopup.setAttribute(
- "ondragstart",
- "PlacesToolbarHelper.onDragStartManaged(event);"
- );
- managedBookmarksPopup.setAttribute(
- "onpopupshowing",
- "PlacesToolbarHelper.populateManagedBookmarks(this);"
- );
- managedBookmarksPopup.setAttribute("placespopup", "true");
- managedBookmarksPopup.setAttribute("is", "places-popup");
- managedBookmarksPopup.classList.add("toolbar-menupopup");
- managedBookmarksButton.appendChild(managedBookmarksPopup);
-
- gNavToolbox.palette.appendChild(managedBookmarksButton);
-
- CustomizableUI.ensureWidgetPlacedInWindow(
- "managed-bookmarks",
- window
- );
-
- // Add button if it doesn't exist
- if (!CustomizableUI.getPlacementOfWidget("managed-bookmarks")) {
- CustomizableUI.addWidgetToArea(
- "managed-bookmarks",
- CustomizableUI.AREA_BOOKMARKS,
- 0
- );
- }
- }
- }
- }
-
- CaptivePortalWatcher.delayedStartup();
-
- ShoppingSidebarManager.ensureInitialized();
-
- SessionStore.promiseAllWindowsRestored.then(() => {
- this._schedulePerWindowIdleTasks();
- document.documentElement.setAttribute("sessionrestored", "true");
- });
-
- this.delayedStartupFinished = true;
- _resolveDelayedStartup();
- Services.obs.notifyObservers(window, "browser-delayed-startup-finished");
- TelemetryTimestamps.add("delayedStartupFinished");
- // We've announced that delayed startup has finished. Do not add code past this point.
- },
-
- /**
- * Resolved on the first MozLayerTreeReady and next MozAfterPaint in the
- * parent process.
- */
- get firstContentWindowPaintPromise() {
- return this._firstContentWindowPaintDeferred.promise;
- },
-
- _setInitialFocus() {
- let initiallyFocusedElement = document.commandDispatcher.focusedElement;
-
- // To prevent startup flicker, the urlbar has the 'focused' attribute set
- // by default. If we are not sure the urlbar will be focused in this
- // window, we need to remove the attribute before first paint.
- // TODO (bug 1629956): The urlbar having the 'focused' attribute by default
- // isn't a useful optimization anymore since UrlbarInput needs layout
- // information to focus the urlbar properly.
- let shouldRemoveFocusedAttribute = true;
-
- this._callWithURIToLoad(uriToLoad => {
- if (
- isBlankPageURL(uriToLoad) ||
- uriToLoad == "about:privatebrowsing" ||
- this.getTabToAdopt()?.isEmpty
- ) {
- gURLBar.select();
- shouldRemoveFocusedAttribute = false;
- return;
- }
-
- // If the initial browser is remote, in order to optimize for first paint,
- // we'll defer switching focus to that browser until it has painted.
- // Otherwise use a regular promise to guarantee that mutationobserver
- // microtasks that could affect focusability have run.
- let promise = gBrowser.selectedBrowser.isRemoteBrowser
- ? this.firstContentWindowPaintPromise
- : Promise.resolve();
-
- promise.then(() => {
- // If focus didn't move while we were waiting, we're okay to move to
- // the browser.
- if (
- document.commandDispatcher.focusedElement == initiallyFocusedElement
- ) {
- gBrowser.selectedBrowser.focus();
- }
- });
- });
-
- // Delay removing the attribute using requestAnimationFrame to avoid
- // invalidating styles multiple times in a row if uriToLoadPromise
- // resolves before first paint.
- if (shouldRemoveFocusedAttribute) {
- window.requestAnimationFrame(() => {
- if (shouldRemoveFocusedAttribute) {
- gURLBar.removeAttribute("focused");
- }
- });
- }
- },
-
- _handleURIToLoad() {
- this._callWithURIToLoad(uriToLoad => {
- if (!uriToLoad) {
- // We don't check whether window.arguments[5] (userContextId) is set
- // because tabbrowser.js takes care of that for the initial tab.
- return;
- }
-
- // We don't check if uriToLoad is a XULElement because this case has
- // already been handled before first paint, and the argument cleared.
- if (Array.isArray(uriToLoad)) {
- // This function throws for certain malformed URIs, so use exception handling
- // so that we don't disrupt startup
- try {
- gBrowser.loadTabs(uriToLoad, {
- inBackground: false,
- replace: true,
- // See below for the semantics of window.arguments. Only the minimum is supported.
- userContextId: window.arguments[5],
- triggeringPrincipal:
- window.arguments[8] ||
- Services.scriptSecurityManager.getSystemPrincipal(),
- allowInheritPrincipal: window.arguments[9],
- csp: window.arguments[10],
- fromExternal: true,
- });
- } catch (e) {}
- } else if (window.arguments.length >= 3) {
- // window.arguments[1]: extraOptions (nsIPropertyBag)
- // [2]: referrerInfo (nsIReferrerInfo)
- // [3]: postData (nsIInputStream)
- // [4]: allowThirdPartyFixup (bool)
- // [5]: userContextId (int)
- // [6]: originPrincipal (nsIPrincipal)
- // [7]: originStoragePrincipal (nsIPrincipal)
- // [8]: triggeringPrincipal (nsIPrincipal)
- // [9]: allowInheritPrincipal (bool)
- // [10]: csp (nsIContentSecurityPolicy)
- // [11]: nsOpenWindowInfo
- let userContextId =
- window.arguments[5] != undefined
- ? window.arguments[5]
- : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
-
- let hasValidUserGestureActivation = undefined;
- let fromExternal = undefined;
- let globalHistoryOptions = undefined;
- let triggeringRemoteType = undefined;
- let forceAllowDataURI = false;
- let wasSchemelessInput = false;
- if (window.arguments[1]) {
- if (!(window.arguments[1] instanceof Ci.nsIPropertyBag2)) {
- throw new Error(
- "window.arguments[1] must be null or Ci.nsIPropertyBag2!"
- );
- }
-
- let extraOptions = window.arguments[1];
- if (extraOptions.hasKey("hasValidUserGestureActivation")) {
- hasValidUserGestureActivation = extraOptions.getPropertyAsBool(
- "hasValidUserGestureActivation"
- );
- }
- if (extraOptions.hasKey("fromExternal")) {
- fromExternal = extraOptions.getPropertyAsBool("fromExternal");
- }
- if (extraOptions.hasKey("triggeringSponsoredURL")) {
- globalHistoryOptions = {
- triggeringSponsoredURL: extraOptions.getPropertyAsACString(
- "triggeringSponsoredURL"
- ),
- };
- if (extraOptions.hasKey("triggeringSponsoredURLVisitTimeMS")) {
- globalHistoryOptions.triggeringSponsoredURLVisitTimeMS =
- extraOptions.getPropertyAsUint64(
- "triggeringSponsoredURLVisitTimeMS"
- );
- }
- }
- if (extraOptions.hasKey("triggeringRemoteType")) {
- triggeringRemoteType = extraOptions.getPropertyAsACString(
- "triggeringRemoteType"
- );
- }
- if (extraOptions.hasKey("forceAllowDataURI")) {
- forceAllowDataURI =
- extraOptions.getPropertyAsBool("forceAllowDataURI");
- }
- if (extraOptions.hasKey("wasSchemelessInput")) {
- wasSchemelessInput =
- extraOptions.getPropertyAsBool("wasSchemelessInput");
- }
- }
-
- try {
- openLinkIn(uriToLoad, "current", {
- referrerInfo: window.arguments[2] || null,
- postData: window.arguments[3] || null,
- allowThirdPartyFixup: window.arguments[4] || false,
- userContextId,
- // pass the origin principal (if any) and force its use to create
- // an initial about:blank viewer if present:
- originPrincipal: window.arguments[6],
- originStoragePrincipal: window.arguments[7],
- triggeringPrincipal: window.arguments[8],
- // TODO fix allowInheritPrincipal to default to false.
- // Default to true unless explicitly set to false because of bug 1475201.
- allowInheritPrincipal: window.arguments[9] !== false,
- csp: window.arguments[10],
- forceAboutBlankViewerInCurrent: !!window.arguments[6],
- forceAllowDataURI,
- hasValidUserGestureActivation,
- fromExternal,
- globalHistoryOptions,
- triggeringRemoteType,
- wasSchemelessInput,
- });
- } catch (e) {
- console.error(e);
- }
-
- window.focus();
- } else {
- // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3.
- // Such callers expect that window.arguments[0] is handled as a single URI.
- loadOneOrMoreURIs(
- uriToLoad,
- Services.scriptSecurityManager.getSystemPrincipal(),
- null
- );
- }
- });
- },
-
- /**
- * Use this function as an entry point to schedule tasks that
- * need to run once per window after startup, and can be scheduled
- * by using an idle callback.
- *
- * The functions scheduled here will fire from idle callbacks
- * once every window has finished being restored by session
- * restore, and after the equivalent only-once tasks
- * have run (from _scheduleStartupIdleTasks in BrowserGlue.sys.mjs).
- */
- _schedulePerWindowIdleTasks() {
- // Bail out if the window has been closed in the meantime.
- if (window.closed) {
- return;
- }
-
- function scheduleIdleTask(func, options) {
- requestIdleCallback(function idleTaskRunner() {
- if (!window.closed) {
- func();
- }
- }, options);
- }
-
- scheduleIdleTask(() => {
- // Initialize the Sync UI
- gSync.init();
- });
-
- scheduleIdleTask(() => {
- // Read prefers-reduced-motion setting
- let reduceMotionQuery = window.matchMedia(
- "(prefers-reduced-motion: reduce)"
- );
- function readSetting() {
- gReduceMotionSetting = reduceMotionQuery.matches;
- }
- reduceMotionQuery.addListener(readSetting);
- readSetting();
- });
-
- scheduleIdleTask(() => {
- // load the tab preview component
- import("chrome://browser/content/tabpreview/tabpreview.mjs").catch(
- console.error
- );
- });
-
- scheduleIdleTask(() => {
- // setup simple gestures support
- gGestureSupport.init(true);
-
- // setup history swipe animation
- gHistorySwipeAnimation.init();
- });
-
- scheduleIdleTask(() => {
- gBrowserThumbnails.init();
- });
-
- scheduleIdleTask(
- () => {
- // Initialize the download manager some time after the app starts so that
- // auto-resume downloads begin (such as after crashing or quitting with
- // active downloads) and speeds up the first-load of the download manager UI.
- // If the user manually opens the download manager before the timeout, the
- // downloads will start right away, and initializing again won't hurt.
- try {
- DownloadsCommon.initializeAllDataLinks();
- ChromeUtils.importESModule(
- "resource:///modules/DownloadsTaskbar.sys.mjs"
- ).DownloadsTaskbar.registerIndicator(window);
- if (AppConstants.platform == "macosx") {
- ChromeUtils.importESModule(
- "resource:///modules/DownloadsMacFinderProgress.sys.mjs"
- ).DownloadsMacFinderProgress.register();
- }
- Services.telemetry.setEventRecordingEnabled("downloads", true);
- } catch (ex) {
- console.error(ex);
- }
- },
- { timeout: 10000 }
- );
-
- if (Win7Features) {
- scheduleIdleTask(() => Win7Features.onOpenWindow());
- }
-
- scheduleIdleTask(async () => {
- NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
- });
-
- scheduleIdleTask(() => {
- gGfxUtils.init();
- });
-
- // This should always go last, since the idle tasks (except for the ones with
- // timeouts) should execute in order. Note that this observer notification is
- // not guaranteed to fire, since the window could close before we get here.
- scheduleIdleTask(() => {
- this.idleTaskPromiseResolve();
- Services.obs.notifyObservers(
- window,
- "browser-idle-startup-tasks-finished"
- );
- });
-
- scheduleIdleTask(() => {
- gProfiles.init();
- });
- },
-
- // Returns the URI(s) to load at startup if it is immediately known, or a
- // promise resolving to the URI to load.
- get uriToLoadPromise() {
- delete this.uriToLoadPromise;
- return (this.uriToLoadPromise = (function () {
- // window.arguments[0]: URI to load (string), or an nsIArray of
- // nsISupportsStrings to load, or a xul:tab of
- // a tabbrowser, which will be replaced by this
- // window (for this case, all other arguments are
- // ignored).
- let uri = window.arguments?.[0];
- if (!uri || window.XULElement.isInstance(uri)) {
- return null;
- }
-
- let defaultArgs = BrowserHandler.defaultArgs;
-
- // If the given URI is different from the homepage, we want to load it.
- if (uri != defaultArgs) {
- AboutNewTab.noteNonDefaultStartup();
-
- if (uri instanceof Ci.nsIArray) {
- // Transform the nsIArray of nsISupportsString's into a JS Array of
- // JS strings.
- return Array.from(
- uri.enumerate(Ci.nsISupportsString),
- supportStr => supportStr.data
- );
- } else if (uri instanceof Ci.nsISupportsString) {
- return uri.data;
- }
- return uri;
- }
-
- // The URI appears to be the the homepage. We want to load it only if
- // session restore isn't about to override the homepage.
- let willOverride = SessionStartup.willOverrideHomepage;
- if (typeof willOverride == "boolean") {
- return willOverride ? null : uri;
- }
- return willOverride.then(willOverrideHomepage =>
- willOverrideHomepage ? null : uri
- );
- })());
- },
-
- // Calls the given callback with the URI to load at startup.
- // Synchronously if possible, or after uriToLoadPromise resolves otherwise.
- _callWithURIToLoad(callback) {
- let uriToLoad = this.uriToLoadPromise;
- if (uriToLoad && uriToLoad.then) {
- uriToLoad.then(callback);
- } else {
- callback(uriToLoad);
- }
- },
-
- onUnload() {
- gUIDensity.uninit();
-
- TabsInTitlebar.uninit();
-
- ToolbarIconColor.uninit();
-
- // In certain scenarios it's possible for unload to be fired before onload,
- // (e.g. if the window is being closed after browser.js loads but before the
- // load completes). In that case, there's nothing to do here.
- if (!this._loadHandled) {
- return;
- }
-
- // First clean up services initialized in gBrowserInit.onLoad (or those whose
- // uninit methods don't depend on the services having been initialized).
-
- CombinedStopReload.uninit();
-
- gGestureSupport.init(false);
-
- gHistorySwipeAnimation.uninit();
-
- FullScreen.uninit();
-
- gSync.uninit();
-
- gExtensionsNotifications.uninit();
- gUnifiedExtensions.uninit();
-
- try {
- gBrowser.removeProgressListener(window.XULBrowserWindow);
- gBrowser.removeTabsProgressListener(window.TabsProgressListener);
- } catch (ex) {}
-
- PlacesToolbarHelper.uninit();
-
- BookmarkingUI.uninit();
-
- TabletModeUpdater.uninit();
-
- gTabletModePageCounter.finish();
-
- CaptivePortalWatcher.uninit();
-
- SidebarUI.uninit();
-
- DownloadsButton.uninit();
-
- if (gToolbarKeyNavEnabled) {
- ToolbarKeyboardNavigator.uninit();
- }
-
- BrowserSearch.uninit();
-
- NewTabPagePreloading.removePreloadedBrowser(window);
-
- FirefoxViewHandler.uninit();
-
- // Now either cancel delayedStartup, or clean up the services initialized from
- // it.
- if (this._boundDelayedStartup) {
- this._cancelDelayedStartup();
- } else {
- if (Win7Features) {
- Win7Features.onCloseWindow();
- }
- Services.prefs.removeObserver(ctrlTab.prefName, ctrlTab);
- ctrlTab.uninit();
- gBrowserThumbnails.uninit();
- gProtectionsHandler.uninit();
- FullZoom.destroy();
-
- Services.obs.removeObserver(gIdentityHandler, "perm-changed");
- Services.obs.removeObserver(gRemoteControl, "devtools-socket");
- Services.obs.removeObserver(gRemoteControl, "marionette-listening");
- Services.obs.removeObserver(gRemoteControl, "remote-listening");
- Services.obs.removeObserver(
- gSessionHistoryObserver,
- "browser:purge-session-history"
- );
- Services.obs.removeObserver(
- gStoragePressureObserver,
- "QuotaManager::StoragePressure"
- );
- Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
- Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
- Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
- Services.obs.removeObserver(
- gXPInstallObserver,
- "addon-install-fullscreen-blocked"
- );
- Services.obs.removeObserver(
- gXPInstallObserver,
- "addon-install-origin-blocked"
- );
- Services.obs.removeObserver(
- gXPInstallObserver,
- "addon-install-policy-blocked"
- );
- Services.obs.removeObserver(
- gXPInstallObserver,
- "addon-install-webapi-blocked"
- );
- Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
- Services.obs.removeObserver(
- gXPInstallObserver,
- "addon-install-confirmation"
- );
- Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup");
-
- MenuTouchModeObserver.uninit();
- BrowserOffline.uninit();
- CanvasPermissionPromptHelper.uninit();
- WebAuthnPromptHelper.uninit();
- PanelUI.uninit();
- }
-
- // Final window teardown, do this last.
- gBrowser.destroy();
- window.XULBrowserWindow = null;
- window.docShell.treeOwner
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIAppWindow).XULBrowserWindow = null;
- window.browserDOMWindow = null;
- },
-};
-
-ChromeUtils.defineLazyGetter(
- gBrowserInit,
- "_firstContentWindowPaintDeferred",
- () => Promise.withResolvers()
-);
-
-gBrowserInit.idleTasksFinishedPromise = new Promise(resolve => {
- gBrowserInit.idleTaskPromiseResolve = 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":
BrowserSearch.webSearch();
break;
case "Bookmarks":
- SidebarUI.toggle("viewBookmarksSidebar");
+ SidebarController.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 +1542,7 @@ function HandleAppCommandEvent(evt) {
openHelpLink("firefox-help");
break;
case "Open":
- BrowserOpenFileWindow();
+ BrowserCommands.openFileWindow();
break;
case "Print":
PrintUtils.startPrintWindow(gBrowser.selectedBrowser.browsingContext);
@@ -2673,203 +1560,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 +1608,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 +1648,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 +1684,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 +1888,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 +2379,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(
@@ -4200,7 +2526,7 @@ const BrowserSearch = {
event
) {
event = getRootEvent(event);
- let where = whereToOpenLink(event);
+ let where = BrowserUtils.whereToOpenLink(event);
if (where == "current") {
// override: historically search opens in new tab
where = "tab";
@@ -4444,7 +2770,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) {
@@ -4505,14 +2832,6 @@ function FillHistoryMenu(aParent) {
return true;
}
-function BrowserDownloadsUI() {
- if (PrivateBrowsingUtils.isWindowPrivate(window)) {
- openTrustedLinkIn("about:downloads", "tab");
- } else {
- PlacesCommandHook.showPlacesOrganizer("Downloads");
- }
-}
-
function toOpenWindowByType(inType, uri, features) {
var topWindow = Services.wm.getMostRecentWindow(inType);
@@ -5037,7 +3356,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 +3389,7 @@ var XULBrowserWindow = {
return gBrowser.tabs.length;
},
- onProgressChange(
- aWebProgress,
- aRequest,
- aCurSelfProgress,
- aMaxSelfProgress,
- aCurTotalProgress,
- aMaxTotalProgress
- ) {
+ onProgressChange() {
// Do nothing.
},
@@ -5420,7 +3732,7 @@ var XULBrowserWindow = {
}
}
- if (TranslationsParent.isRestrictedPage(gBrowser)) {
+ if (TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser)) {
this._menuItemForTranslations.setAttribute("disabled", "true");
} else {
this._menuItemForTranslations.removeAttribute("disabled");
@@ -5576,7 +3888,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 +3931,7 @@ var XULBrowserWindow = {
aStateFlags,
aStatus,
aMessage,
- aTotalProgress
+ _aTotalProgress
) {
if (FullZoom.updateBackgroundTabs) {
FullZoom.onLocationChange(gBrowser.currentURI, true);
@@ -6087,7 +4399,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;
}
@@ -6228,7 +4540,7 @@ nsBrowserAccess.prototype = {
: PrivateBrowsingUtils.isWindowPrivate(window);
switch (aWhere) {
- case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW:
+ case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW: {
// FIXME: Bug 408379. So how come this doesn't send the
// referrer like the other loads do?
var url = aURI && aURI.spec;
@@ -6272,6 +4584,7 @@ nsBrowserAccess.prototype = {
console.error(ex);
}
break;
+ }
case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB:
case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND: {
// If we have an opener, that means that the caller is expecting access
@@ -6682,7 +4995,7 @@ function setToolbarVisibility(
document.documentElement.toggleAttribute(overlapAttr, false);
break;
case "newtab":
- default:
+ default: {
let currentURI = gBrowser?.currentURI;
if (!gBrowserInit.domContentLoaded) {
let uriToLoad = gBrowserInit.uriToLoadPromise;
@@ -6699,6 +5012,7 @@ function setToolbarVisibility(
isVisible = BookmarkingUI.isOnNewTabPage(currentURI);
document.documentElement.toggleAttribute(overlapAttr, isVisible);
break;
+ }
}
}
@@ -6801,7 +5115,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.
@@ -6855,9 +5169,10 @@ var gUIDensity = {
}
let docs = [document.documentElement];
- let shouldUpdateSidebar = SidebarUI.initialized && SidebarUI.isOpen;
+ let shouldUpdateSidebar =
+ SidebarController.initialized && SidebarController.isOpen;
if (shouldUpdateSidebar) {
- docs.push(SidebarUI.browser.contentDocument.documentElement);
+ docs.push(SidebarController.browser.contentDocument.documentElement);
}
for (let doc of docs) {
switch (mode) {
@@ -6873,7 +5188,7 @@ var gUIDensity = {
}
}
if (shouldUpdateSidebar) {
- let tree = SidebarUI.browser.contentDocument.querySelector(
+ let tree = SidebarController.browser.contentDocument.querySelector(
".sidebar-placesTree"
);
if (tree) {
@@ -7110,7 +5425,7 @@ function handleLinkClick(event, href, linkNode) {
return false;
}
- var where = whereToOpenLink(event);
+ var where = BrowserUtils.whereToOpenLink(event);
if (where == "current") {
return false;
}
@@ -7183,7 +5498,7 @@ function middleMousePaste(event) {
// if it's not the current tab, we don't need to do anything because the
// browser doesn't exist.
- let where = whereToOpenLink(event, true, false);
+ let where = BrowserUtils.whereToOpenLink(event, true, false);
let lastLocationChange;
if (where == "current") {
lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
@@ -7297,11 +5612,6 @@ function handleDroppedLink(
}
}
-function BrowserForceEncodingDetection() {
- gBrowser.selectedBrowser.forceEncodingDetection();
- BrowserReloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
-}
-
var ToolbarContextMenu = {
updateDownloadsAutoHide(popup) {
let checkbox = document.getElementById(
@@ -7467,7 +5777,7 @@ var BrowserOffline = {
},
// nsIObserver
- observe(aSubject, aTopic, aState) {
+ observe(aSubject, aTopic) {
if (aTopic != "network:offline-status-changed") {
return;
}
@@ -7695,8 +6005,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 +6125,7 @@ var WebAuthnPromptHelper = {
secondaryActions.push({
label,
accessKey: i.toString(),
- callback(aState) {
+ callback() {
mgr.selectionCallback(tid, i);
},
});
@@ -7859,9 +6169,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 +6193,6 @@ var WebAuthnPromptHelper = {
let options = {
learnMoreURL,
- checkbox: {
- label: gNavigatorBundle.getString("webauthn.anonymize"),
- },
hintText: "webauthn.registerDirectPromptHint",
};
this.show(
@@ -7994,16 +6315,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"),
@@ -8195,84 +6506,6 @@ var MailIntegration = {
},
};
-/**
- * Open about:addons page by given view id.
- * @param {String} aView
- * View id of page that will open.
- * e.g. "addons://discover/"
- * @param {Object} options
- * {
- * selectTabByViewId: If true, if there is the tab opening page having
- * same view id, select the tab. Else if the current
- * page is blank, load on it. Otherwise, open a new
- * tab, then load on it.
- * If false, if there is the tab opening
- * about:addoons page, select the tab and load page
- * for view id on it. Otherwise, leave the loading
- * behavior to switchToTabHavingURI().
- * If no options, handles as false.
- * }
- * @returns {Promise} When the Promise resolves, returns window object loaded the
- * view id.
- */
-function BrowserOpenAddonsMgr(aView, { selectTabByViewId = false } = {}) {
- return new Promise(resolve => {
- let emWindow;
- let browserWindow;
-
- var receivePong = function (aSubject, aTopic, aData) {
- let browserWin = aSubject.browsingContext.topChromeWindow;
- if (!emWindow || browserWin == window /* favor the current window */) {
- if (
- selectTabByViewId &&
- aSubject.gViewController.currentViewId !== aView
- ) {
- return;
- }
-
- emWindow = aSubject;
- browserWindow = browserWin;
- }
- };
- Services.obs.addObserver(receivePong, "EM-pong");
- Services.obs.notifyObservers(null, "EM-ping");
- Services.obs.removeObserver(receivePong, "EM-pong");
-
- if (emWindow) {
- if (aView && !selectTabByViewId) {
- emWindow.loadView(aView);
- }
- let tab = browserWindow.gBrowser.getTabForBrowser(
- emWindow.docShell.chromeEventHandler
- );
- browserWindow.gBrowser.selectedTab = tab;
- emWindow.focus();
- resolve(emWindow);
- return;
- }
-
- if (selectTabByViewId) {
- const target = isBlankPageURL(gBrowser.currentURI.spec)
- ? "current"
- : "tab";
- openTrustedLinkIn("about:addons", target);
- } else {
- // This must be a new load, else the ping/pong would have
- // found the window above.
- switchToTabHavingURI("about:addons", true);
- }
-
- Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
- Services.obs.removeObserver(observer, aTopic);
- if (aView) {
- aSubject.loadView(aView);
- }
- aSubject.focus();
- resolve(aSubject);
- }, "EM-loaded");
- });
-}
-
function AddKeywordForSearchField() {
if (!gContextMenu) {
throw new Error("Context menu doesn't seem to be open.");
@@ -8449,7 +6682,7 @@ function ReportSiteIssue() {
* and when the "remote-listening" system notification fires.
*/
const gRemoteControl = {
- observe(subject, topic, data) {
+ observe() {
gRemoteControl.updateVisualCue();
},
@@ -8658,7 +6891,7 @@ function switchToTabHavingURI(
ignoreQueryString || replaceQueryString,
ignoreFragmentWhenComparing
);
- let browserUserContextId = browser.getAttribute("usercontextid");
+ let browserUserContextId = browser.getAttribute("usercontextid") || "";
if (aUserContextId != null && aUserContextId != browserUserContextId) {
continue;
}
@@ -8823,7 +7056,7 @@ function safeModeRestart() {
*/
function duplicateTabIn(aTab, where, delta) {
switch (where) {
- case "window":
+ case "window": {
let otherWin = OpenBrowserWindow({
private: PrivateBrowsingUtils.isBrowserPrivate(aTab.linkedBrowser),
});
@@ -8845,6 +7078,7 @@ function duplicateTabIn(aTab, where, delta) {
"browser-delayed-startup-finished"
);
break;
+ }
case "tabshifted":
SessionStore.duplicateTab(window, aTab, delta);
// A background tab has been opened, nothing else to do here.
@@ -9447,221 +7681,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 = {
@@ -9907,15 +7926,14 @@ var ConfirmationHint = {
* - event (DOM event): The event that triggered the feedback
* - descriptionId (string): message ID of the description text
* - position (string): position of the panel relative to the anchor.
- *
+ * - l10nArgs (object): l10n arguments for the messageId.
*/
show(anchor, messageId, options = {}) {
this._reset();
MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl");
MozXULElement.insertFTLIfNeeded("browser/confirmationHints.ftl");
- document.l10n.setAttributes(this._message, messageId);
-
+ document.l10n.setAttributes(this._message, messageId, options.l10nArgs);
if (options.descriptionId) {
document.l10n.setAttributes(this._description, options.descriptionId);
this._description.hidden = false;
@@ -10018,11 +8036,9 @@ var FirefoxViewHandler = {
ChromeUtils.defineESModuleGetters(this, {
SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
});
- Services.obs.addObserver(this, "firefoxview-notification-dot-update");
},
uninit() {
CustomizableUI.removeListener(this);
- Services.obs.removeObserver(this, "firefoxview-notification-dot-update");
},
onWidgetRemoved(aWidgetId) {
if (aWidgetId == this.BUTTON_ID && this.tab) {
@@ -10075,7 +8091,7 @@ var FirefoxViewHandler = {
},
handleEvent(e) {
switch (e.type) {
- case "TabSelect":
+ case "TabSelect": {
const selected = e.target == this.tab;
this.button?.toggleAttribute("open", selected);
this.button?.setAttribute("aria-pressed", selected);
@@ -10086,6 +8102,7 @@ var FirefoxViewHandler = {
gBrowser.visibleTabs[0].style.MozUserFocus =
e.target == this.tab ? "normal" : "";
break;
+ }
case "TabClose":
this.tab = null;
gBrowser.tabContainer.removeEventListener("TabSelect", this);
@@ -10096,14 +8113,6 @@ var FirefoxViewHandler = {
break;
}
},
- observe(sub, topic, data) {
- switch (topic) {
- case "firefoxview-notification-dot-update":
- let shouldShow = data === "true";
- this._toggleNotificationDot(shouldShow);
- break;
- }
- },
_closeDeviceConnectedTab() {
if (!TabsSetupFlowManager.didFxaTabOpen) {
return;
@@ -10134,11 +8143,6 @@ var FirefoxViewHandler = {
_onTabForegrounded() {
if (this.tab?.selected) {
this.SyncedTabs.syncTabs();
- Services.obs.notifyObservers(
- null,
- "firefoxview-notification-dot-update",
- "false"
- );
}
},
_recordViewIfTabSelected() {
@@ -10162,7 +8166,4 @@ var FirefoxViewHandler = {
}
}
},
- _toggleNotificationDot(shouldShow) {
- this.button?.toggleAttribute("attention", shouldShow);
- },
};
diff --git a/browser/base/content/browser.js.globals b/browser/base/content/browser.js.globals
index c767bb5beb..910e0b5ca9 100644
--- a/browser/base/content/browser.js.globals
+++ b/browser/base/content/browser.js.globals
@@ -28,41 +28,20 @@
"gPopupBlockerObserver",
"gKeywordURIFixup",
"_createNullPrincipalFromTabUserContextId",
- "_resolveDelayedStartup",
- "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",
@@ -72,7 +51,6 @@
"BrowserSearch",
"CreateContainerTabMenu",
"FillHistoryMenu",
- "BrowserDownloadsUI",
"toOpenWindowByType",
"OpenBrowserWindow",
"updateEditUIVisibility",
@@ -103,7 +81,6 @@
"handleLinkClick",
"middleMousePaste",
"handleDroppedLink",
- "BrowserForceEncodingDetection",
"ToolbarContextMenu",
"BrowserOffline",
"CanvasPermissionPromptHelper",
@@ -112,7 +89,6 @@
"WindowIsClosing",
"warnAboutClosingWindow",
"MailIntegration",
- "BrowserOpenAddonsMgr",
"AddKeywordForSearchField",
"restoreLastClosedTabOrWindowOrSession",
"undoCloseTab",
@@ -131,7 +107,6 @@
"PanicButtonNotifier",
"SafeBrowsingNotificationBox",
"TabDialogBox",
- "TabModalPromptBox",
"gDialogBox",
"ConfirmationHint",
"FirefoxViewHandler",
@@ -154,7 +129,6 @@
"DownloadsCommon",
"E10SUtils",
"ExtensionsUI",
- "FirefoxViewNotificationManager",
"HomePage",
"isProductURL",
"LightweightThemeConsumer",
@@ -196,7 +170,6 @@
"SubDialog",
"SubDialogManager",
"TabCrashHandler",
- "TabModalPrompt",
"TabsSetupFlowManager",
"TelemetryEnvironment",
"TranslationsParent",
diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml
index 1dcdd02cd1..aec0983a67 100644
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -38,11 +38,7 @@
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
rel="stylesheet"
href="chrome://browser/content/downloads/downloads.css"
@@ -87,7 +83,6 @@
<link rel="localization" href="browser/translations.ftl" />
<link rel="localization" href="browser/unifiedExtensions.ftl"/>
<link rel="localization" href="browser/webrtcIndicator.ftl"/>
- <link rel="localization" href="toolkit/branding/accounts.ftl"/>
<link rel="localization" href="toolkit/branding/brandings.ftl"/>
<link rel="localization" href="toolkit/global/contextual-identity.ftl"/>
<link rel="localization" href="toolkit/global/textActions.ftl"/>
@@ -95,9 +90,9 @@
<!-- Untranslated FTL files -->
<link rel="localization" href="preview/enUS-searchFeatures.ftl" />
<link rel="localization" href="preview/interventions.ftl" />
- <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"/>
@@ -110,6 +105,7 @@
<script>
/* eslint-env mozilla/browser-window */
+ Services.scriptloader.loadSubScript("chrome://browser/content/browser-init.js", this);
Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/browser-captivePortal.js", this);
if (AppConstants.MOZ_DATA_REPORTING) {
@@ -119,7 +115,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/macWindow.inc.xhtml b/browser/base/content/macWindow.inc.xhtml
index b9ca44c7eb..b5afa5f644 100644
--- a/browser/base/content/macWindow.inc.xhtml
+++ b/browser/base/content/macWindow.inc.xhtml
@@ -16,7 +16,6 @@
<html:link rel="localization" href="browser/menubar.ftl"/>
<html:link rel="localization" href="browser/reportBrokenSite.ftl"/>
<html:link rel="localization" href="browser/screenshots.ftl"/>
- <html:link rel="localization" href="toolkit/branding/accounts.ftl"/>
<html:link rel="localization" href="toolkit/branding/brandings.ftl"/>
<html:link rel="localization" href="toolkit/global/textActions.ftl"/>
</linkset>
diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml
index bff8d98b27..ef8245938e 100644
--- a/browser/base/content/main-popupset.inc.xhtml
+++ b/browser/base/content/main-popupset.inc.xhtml
@@ -77,13 +77,16 @@
data-lazy-l10n-id="tab-context-close-n-tabs"
data-l10n-args='{"tabCount": 1}'
oncommand="TabContextMenu.closeContextTabs();"/>
+ <menuitem id="context_closeDuplicateTabs"
+ data-lazy-l10n-id="tab-context-close-duplicate-tabs"
+ oncommand="gBrowser.removeDuplicateTabs(TabContextMenu.contextTab);"/>
<menu id="context_closeTabOptions"
data-lazy-l10n-id="tab-context-close-multiple-tabs">
<menupopup id="closeTabOptions">
<menuitem id="context_closeTabsToTheStart" data-lazy-l10n-id="close-tabs-to-the-start"
- oncommand="gBrowser.removeTabsToTheStartFrom(TabContextMenu.contextTab, {animate: true});"/>
+ oncommand="gBrowser.removeTabsToTheStartFrom(TabContextMenu.contextTab);"/>
<menuitem id="context_closeTabsToTheEnd" data-lazy-l10n-id="close-tabs-to-the-end"
- oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab, {animate: true});"/>
+ oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab);"/>
<menuitem id="context_closeOtherTabs" data-lazy-l10n-id="close-other-tabs"
oncommand="gBrowser.removeAllTabsBut(TabContextMenu.contextTab);"/>
</menupopup>
@@ -100,7 +103,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 +112,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"/>
@@ -253,23 +256,26 @@
<menuitem id="sidebar-switcher-bookmarks"
data-l10n-id="sidebar-menu-bookmarks"
key="viewBookmarksSidebarKb"
- oncommand="SidebarUI.show('viewBookmarksSidebar');"/>
+ oncommand="SidebarController.show('viewBookmarksSidebar');"/>
<menuitem id="sidebar-switcher-history"
data-l10n-id="sidebar-menu-history"
key="key_gotoHistory"
- oncommand="SidebarUI.show('viewHistorySidebar');"/>
+ oncommand="SidebarController.show('viewHistorySidebar');"/>
<menuitem id="sidebar-switcher-tabs"
data-l10n-id="sidebar-menu-synced-tabs"
class="sync-ui-item"
- oncommand="SidebarUI.show('viewTabsSidebar');"/>
+ oncommand="SidebarController.show('viewTabsSidebar');"/>
+ <menuitem id="sidebar-switcher-megalist"
+ data-l10n-id="sidebar-menu-megalist"
+ oncommand="SidebarController.show('viewMegalistSidebar');"/>
<menuseparator/>
<!-- Extension toolbarbuttons go here. -->
<menuseparator id="sidebar-extensions-separator"/>
<menuitem id="sidebar-reverse-position"
- oncommand="SidebarUI.reversePosition()"/>
+ oncommand="SidebarController.reversePosition()"/>
<menuseparator/>
<menuitem data-l10n-id="sidebar-menu-close"
- oncommand="SidebarUI.hide()"/>
+ oncommand="SidebarController.hide()"/>
</menupopup>
<menupopup id="toolbar-context-menu"
@@ -360,7 +366,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 +418,24 @@
<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 class="tab-preview-pid-activeness">
+ <html:div class="tab-preview-pid"></html:div>
+ <html:div class="tab-preview-activeness"></html:div>
+ </html:div>
+ </html:div>
+ <html:div class="tab-preview-thumbnail-container"></html:div>
+ </panel>
<html:template id="pageActionPanelTemplate">
<panel id="pageActionPanel"
@@ -624,7 +647,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"
@@ -659,4 +682,14 @@
<menuitem data-l10n-id="translations-panel-settings-about2"
oncommand="FullPageTranslationsPanel.onAboutTranslations()"/>
</menupopup>
+
+ <menupopup id="select-translations-panel-settings-menupopup">
+ <menuitem id="select-translations-panel-open-settings-page-menuitem"
+ class="manage-languages-menuitem"
+ data-l10n-id="select-translations-panel-open-translations-settings-menuitem"
+ oncommand="SelectTranslationsPanel.openTranslationsSettingsPage()"/>
+ <menuitem id="select-translations-panel-about-translations-menuitem"
+ data-l10n-id="translations-panel-settings-about2"
+ oncommand="SelectTranslationsPanel.onAboutTranslations()"/>
+ </menupopup>
</popupset>
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index fc19910726..0a67afa81f 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -100,6 +100,9 @@
<image class="private-browsing-indicator-icon"/>
<label data-l10n-id="private-browsing-indicator-label"></label>
</hbox>
+ <toolbarbutton id="content-analysis-indicator"
+ oncommand="ContentAnalysis.showPanel(this, PanelUI);"
+ class="toolbarbutton-1 content-analysis-indicator-icon"/>
#include titlebar-items.inc.xhtml
@@ -491,13 +494,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"
@@ -516,12 +512,12 @@
customizable="true">
<toolbartabstop skipintoolbarset="true"/>
- <hbox id="personal-toolbar-empty" skipintoolbarset="true" removable="false" hidden="true">
+ <hbox id="personal-toolbar-empty" skipintoolbarset="true" removable="false" hidden="true" role="alert">
<description id="personal-toolbar-empty-description"
data-l10n-id="bookmarks-toolbar-empty-message"
onclick="BookmarkingUI.openLibraryIfLinkClicked(event);"
onkeydown="BookmarkingUI.openLibraryIfLinkClicked(event);">
- <html:a data-l10n-name="manage-bookmarks" class="text-link" tabindex="0"/>
+ <html:a data-l10n-name="manage-bookmarks" class="text-link" tabindex="0" role="link"/>
</description>
</hbox>
@@ -625,7 +621,7 @@
<menuitem id="BMB_viewBookmarksSidebar"
data-l10n-id="bookmarks-tools-sidebar-visibility"
data-l10n-args='{ "isVisible": false }'
- oncommand="SidebarUI.toggle('viewBookmarksSidebar');"
+ oncommand="SidebarController.toggle('viewBookmarksSidebar');"
key="viewBookmarksSidebarKb"/>
<menuitem id="BMB_searchBookmarks"
data-l10n-id="bookmarks-search"
@@ -708,7 +704,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..1811dfcaca 100644
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1347,8 +1347,6 @@ class nsContextMenu {
!this.onTextInput &&
!this.onLink &&
!this.onPlainTextLink &&
- !this.onImage &&
- !this.onVideo &&
!this.onAudio &&
!this.onEditable &&
!this.onPassword;
@@ -1619,7 +1617,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 +1625,7 @@ class nsContextMenu {
}
viewInfo() {
- BrowserPageInfo(
+ BrowserCommands.pageInfo(
this.contentData.docLocation,
null,
null,
@@ -1637,7 +1635,7 @@ class nsContextMenu {
}
viewImageInfo() {
- BrowserPageInfo(
+ BrowserCommands.pageInfo(
this.contentData.docLocation,
"mediaTab",
this.imageInfo,
@@ -1661,7 +1659,7 @@ class nsContextMenu {
}
viewFrameInfo() {
- BrowserPageInfo(
+ BrowserCommands.pageInfo(
this.contentData.docLocation,
null,
null,
@@ -1685,7 +1683,7 @@ class nsContextMenu {
// Change current window to the URL of the image, video, or audio.
viewMedia(e) {
- let where = whereToOpenLink(e, false, false);
+ let where = BrowserUtils.whereToOpenLink(e, false, false);
if (where == "current") {
where = "tab";
}
@@ -1982,7 +1980,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);
},
};
@@ -2175,7 +2173,10 @@ class nsContextMenu {
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
Ci.nsIClipboardHelper
);
- clipboard.copyString(addresses);
+ clipboard.copyString(
+ addresses,
+ this.actor.manager.browsingContext.currentWindowGlobal
+ );
}
// Extract phone and put it on clipboard
@@ -2195,7 +2196,10 @@ class nsContextMenu {
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
Ci.nsIClipboardHelper
);
- clipboard.copyString(phone);
+ clipboard.copyString(
+ phone,
+ this.actor.manager.browsingContext.currentWindowGlobal
+ );
}
copyLink() {
@@ -2204,7 +2208,10 @@ class nsContextMenu {
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
Ci.nsIClipboardHelper
);
- clipboard.copyString(linkURL);
+ clipboard.copyString(
+ linkURL,
+ this.actor.manager.browsingContext.currentWindowGlobal
+ );
}
/**
@@ -2220,7 +2227,10 @@ class nsContextMenu {
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
Ci.nsIClipboardHelper
);
- clipboard.copyString(strippedLinkURL);
+ clipboard.copyString(
+ strippedLinkURL,
+ this.actor.manager.browsingContext.currentWindowGlobal
+ );
}
}
@@ -2324,8 +2334,8 @@ class nsContextMenu {
try {
strippedLinkURI = QueryStringStripper.stripForCopyOrShare(this.linkURI);
} catch (e) {
- console.warn(`isLinkURIStrippable: ${e.message}`);
- return null;
+ console.warn(`stripForCopyOrShare: ${e.message}`);
+ return this.linkURI;
}
// If nothing can be stripped, we return the original URI
@@ -2458,7 +2468,10 @@ class nsContextMenu {
var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
Ci.nsIClipboardHelper
);
- clipboard.copyString(this.originalMediaURL);
+ clipboard.copyString(
+ this.originalMediaURL,
+ this.actor.manager.browsingContext.currentWindowGlobal
+ );
}
getImageText() {
@@ -2484,7 +2497,7 @@ class nsContextMenu {
let drmInfoURL =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"drm-content";
- let dest = whereToOpenLink(aEvent);
+ let dest = BrowserUtils.whereToOpenLink(aEvent);
// Don't ever want this to open in the same tab as it'll unload the
// DRM'd video, which is going to be a bad idea in most cases.
if (dest == "current") {
@@ -2494,23 +2507,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 +2514,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 +2530,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
+ ).catch(console.error);
}
/**
@@ -2583,6 +2590,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,14 +2614,13 @@ 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.
!(translationsEnabled && selectTranslationsEnabled) ||
+ // Only show the item if Translations is supported on this hardware.
+ !TranslationsParent.getIsTranslationsEngineSupported() ||
// If there is no text to translate, we have nothing to do.
textToTranslate.length === 0 ||
// We do not allow translating selections on top of Full Page Translations.
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/popup-notifications.inc b/browser/base/content/popup-notifications.inc
index daee34e6fe..3d57f4808c 100644
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -35,7 +35,7 @@
<description id="webRTC-all-windows-shared" hidden="true" data-l10n-id="popup-all-windows-shared"></description>
</popupnotificationcontent>
- <popupnotificationcontent id="webRTC-preview" hidden="true">
+ <popupnotificationcontent id="webRTC-preview" orient="vertical" hidden="true">
<html:video id="webRTC-previewVideo" tabindex="-1"/>
<vbox id="webRTC-previewWarningBox">
<description id="webRTC-previewWarning"/>
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/spotlight.html b/browser/base/content/spotlight.html
index a948f8dbf4..216f290efd 100644
--- a/browser/base/content/spotlight.html
+++ b/browser/base/content/spotlight.html
@@ -19,6 +19,7 @@
<link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<link rel="localization" href="browser/newtab/asrouter.ftl" />
+ <link rel="localization" href="preview/onboarding.ftl" />
<link rel="localization" href="browser/newtab/onboarding.ftl" />
<link rel="localization" href="browser/spotlight.ftl" />
<link rel="localization" href="browser/migrationWizard.ftl" />
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..9b30584077 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;
@@ -123,26 +125,17 @@
this.tabbox.tabpanels.setAttribute("async", "true");
}
- 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);
- };
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_showCardPreviews",
TAB_PREVIEW_PREF,
- false,
- () => this.configureTooltip()
+ false
);
- this.configureTooltip();
+ this.tooltip = "tabbrowser-tab-tooltip";
+ this._previewPanel = null;
}
- on_TabSelect(event) {
+ on_TabSelect() {
this._handleTabSelect();
}
@@ -188,23 +181,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 +234,7 @@
}
if (!this._blockDblClick) {
- BrowserOpenTab();
+ BrowserCommands.openTab();
}
event.preventDefault();
@@ -333,7 +326,7 @@
(!RTL_UI && event.clientX > endOfTab) ||
(RTL_UI && event.clientX < endOfTab)
) {
- BrowserOpenTab();
+ BrowserCommands.openTab();
}
} else {
return;
@@ -450,6 +443,7 @@
return;
}
+ this._previewPanel?.deactivate();
this.startTabDrag(event, tab);
}
@@ -1092,6 +1086,10 @@
return children;
}
+ get previewPanel() {
+ return this._previewPanel;
+ }
+
_getVisibleTabs() {
if (!this._visibleTabs) {
this._visibleTabs = Array.prototype.filter.call(
@@ -1205,7 +1203,7 @@
};
}
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
switch (aTopic) {
case "nsPref:changed":
// This is has to deal with changes in
@@ -1851,6 +1849,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.css b/browser/base/content/tabbrowser.css
deleted file mode 100644
index 120203141c..0000000000
--- a/browser/base/content/tabbrowser.css
+++ /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/. */
-
-.tab-close-button[pinned],
-#tabbrowser-tabs[closebuttons="activetab"] > #tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-content > .tab-close-button:not([selected]),
-.tab-icon-pending:not([pendingicon]),
-.tab-icon-pending[busy],
-.tab-icon-pending[pinned],
-.tab-icon-image:not([src], [pinned], [crashed], [pictureinpicture])[selected],
-.tab-icon-image:not([src], [pinned], [crashed], [sharing], [pictureinpicture]),
-.tab-icon-image[busy],
-.tab-throbber:not([busy]),
-.tab-sharing-icon-overlay,
-.tab-icon-overlay {
- display: none;
-}
-
-:root[uidensity=compact] .tab-secondary-label,
-.tab-secondary-label:not([soundplaying], [muted], [activemedia-blocked], [pictureinpicture]),
-.tab-secondary-label:not([activemedia-blocked]) > .tab-icon-sound-blocked-label,
-.tab-secondary-label[muted][activemedia-blocked] > .tab-icon-sound-blocked-label,
-.tab-secondary-label[activemedia-blocked] > .tab-icon-sound-playing-label,
-.tab-secondary-label[muted] > .tab-icon-sound-playing-label,
-.tab-secondary-label[pictureinpicture] > .tab-icon-sound-playing-label,
-.tab-secondary-label[pictureinpicture] > .tab-icon-sound-muted-label,
-.tab-secondary-label:not([pictureinpicture]) > .tab-icon-sound-pip-label,
-.tab-secondary-label:not([muted]) > .tab-icon-sound-muted-label,
-.tab-secondary-label:not([showtooltip]) > .tab-icon-sound-tooltip-label,
-.tab-secondary-label[showtooltip] > .tab-icon-sound-label:not(.tab-icon-sound-tooltip-label) {
- display: none;
-}
-
-.tab-sharing-icon-overlay[sharing]:not([selected]),
-.tab-icon-overlay:is([soundplaying], [muted], [activemedia-blocked], [crashed]) {
- display: revert;
-}
-
-.tabbrowser-tab {
- --tab-label-mask-size: 2em;
-}
-
-.tab-label {
- white-space: nowrap;
- line-height: 1.7; /* override 'normal' in case of fallback math fonts with huge metrics */
-}
-
-.tab-secondary-label {
- margin: -.3em 0 .3em; /* adjust margins to compensate for line-height of .tab-label */
-}
-
-.tab-label-container {
- overflow: hidden;
-}
-
-.tab-label-container[pinned] {
- width: 0;
-}
-
-.tab-label-container[textoverflow][labeldirection=ltr]:not([pinned]),
-.tab-label-container[textoverflow]:not([labeldirection], [pinned]):-moz-locale-dir(ltr) {
- direction: ltr;
- mask-image: linear-gradient(to left, transparent, black var(--tab-label-mask-size));
-}
-
-.tab-label-container[textoverflow][labeldirection=rtl]:not([pinned]),
-.tab-label-container[textoverflow]:not([labeldirection], [pinned]):-moz-locale-dir(rtl) {
- direction: rtl;
- mask-image: linear-gradient(to right, transparent, black var(--tab-label-mask-size));
-}
-
-tabpanels {
- background-color: transparent;
-}
-
-/* Apply crisp rendering for favicons at exactly 2dppx resolution */
-@media (resolution: 2dppx) {
- .tab-icon-image {
- image-rendering: -moz-crisp-edges;
- }
-}
-
-.closing-tabs-spacer {
- pointer-events: none;
-}
-
-#tabbrowser-arrowscrollbox:not(:hover) > #tabbrowser-arrowscrollbox-periphery > .closing-tabs-spacer {
- transition: width .15s ease-out;
-}
-
-browser[blank],
-browser[pendingpaint] {
- opacity: 0;
-}
-
-#tabbrowser-tabpanels[pendingpaint] {
- background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
- background-repeat: no-repeat;
- background-position: center center;
- background-size: 30px;
-}
diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js
index 54a801939a..f319fd5d46 100644
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -67,7 +67,7 @@
replaceContainerClass("color", hbox, identity.color);
let label = ContextualIdentityService.getUserContextLabel(userContextId);
- document.getElementById("userContext-label").setAttribute("value", label);
+ document.getElementById("userContext-label").textContent = label;
// Also set the container label as the tooltip so we can only show the icon
// in small windows.
hbox.setAttribute("tooltiptext", label);
@@ -110,6 +110,12 @@
"privacy.exposeContentTitleInWindow.pbm",
true
);
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_showTabCardPreview",
+ "browser.tabs.cardPreview.enabled",
+ true
+ );
if (AppConstants.MOZ_CRASHREPORTER) {
ChromeUtils.defineESModuleGetters(this, {
@@ -161,6 +167,8 @@
TO_START: 2,
TO_END: 3,
MULTI_SELECTED: 4,
+ DUPLICATES: 6,
+ ALL_DUPLICATES: 7,
},
_lastRelatedTabMap: new WeakMap(),
@@ -348,6 +356,90 @@
return this.tabContainer._getVisibleTabs();
},
+ getDuplicateTabsToClose(aTab) {
+ // One would think that a set is better, but it would need to copy all
+ // the strings instead of just keeping references to the nsIURI objects,
+ // and the array is presumed to be small anyways.
+ let keys = [];
+ let keyForTab = tab => {
+ let uri = tab.linkedBrowser?.currentURI;
+ if (!uri) {
+ return null;
+ }
+ return {
+ uri,
+ userContextId: tab.userContextId,
+ };
+ };
+ let keyEquals = (a, b) => {
+ return a.userContextId == b.userContextId && a.uri.equals(b.uri);
+ };
+ if (aTab.multiselected) {
+ for (let tab of this.selectedTabs) {
+ let key = keyForTab(tab);
+ if (key) {
+ keys.push(key);
+ }
+ }
+ } else {
+ let key = keyForTab(aTab);
+ if (key) {
+ keys.push(key);
+ }
+ }
+
+ if (!keys.length) {
+ return [];
+ }
+
+ let duplicateTabs = [];
+ for (let tab of this.tabs) {
+ if (tab == aTab || tab.pinned) {
+ continue;
+ }
+ if (aTab.multiselected && tab.multiselected) {
+ continue;
+ }
+ let key = keyForTab(tab);
+ if (key && keys.some(k => keyEquals(k, key))) {
+ duplicateTabs.push(tab);
+ }
+ }
+
+ return duplicateTabs;
+ },
+
+ getAllDuplicateTabsToClose() {
+ let lastSeenTabs = this.tabs.toSorted(
+ (a, b) => b.lastSeenActive - a.lastSeenActive
+ );
+ let duplicateTabs = [];
+ let keys = [];
+ for (let tab of lastSeenTabs) {
+ const uri = tab.linkedBrowser?.currentURI;
+ if (!uri) {
+ // Can't tell if it's a duplicate without a URI.
+ // Safest to leave it be.
+ continue;
+ }
+
+ const key = {
+ uri,
+ userContextId: tab.userContextId,
+ };
+ if (
+ !tab.pinned &&
+ keys.some(
+ k => k.userContextId == key.userContextId && k.uri.equals(key.uri)
+ )
+ ) {
+ duplicateTabs.push(tab);
+ }
+ keys.push(key);
+ }
+ return duplicateTabs;
+ },
+
get _numPinnedTabs() {
for (var i = 0; i < this.tabs.length; i++) {
if (!this.tabs[i].pinned) {
@@ -893,14 +985,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");
@@ -1116,6 +1200,11 @@
return;
}
+ let oldBrowser = this.selectedBrowser;
+ // Once the async switcher starts, it's unpredictable when it will touch
+ // the address bar, thus we store its state immediately.
+ gURLBar?.saveSelectionStateForBrowser(oldBrowser);
+
let newTab = this.getTabForBrowser(newBrowser);
if (!aForceUpdate) {
@@ -1145,8 +1234,6 @@
}
this._lastRelatedTabMap = new WeakMap();
- let oldBrowser = this.selectedBrowser;
-
if (!gMultiProcessBrowser) {
oldBrowser.removeAttribute("primary");
oldBrowser.docShellIsActive = false;
@@ -1154,11 +1241,6 @@
newBrowser.docShellIsActive = !document.hidden;
}
- if (gURLBar) {
- oldBrowser._urlbarSelectionStart = gURLBar.selectionStart;
- oldBrowser._urlbarSelectionEnd = gURLBar.selectionEnd;
- }
-
this._selectedBrowser = newBrowser;
this._selectedTab = newTab;
this.showTab(newTab);
@@ -1320,31 +1402,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 +1496,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.
@@ -1491,19 +1535,12 @@
if (currentActiveElement != document.activeElement) {
return;
}
-
- gURLBar.setSelectionRange(
- newBrowser._urlbarSelectionStart,
- newBrowser._urlbarSelectionEnd
- );
+ gURLBar.restoreSelectionStateForBrowser(newBrowser);
},
{ once: true }
);
} else {
- gURLBar.setSelectionRange(
- newBrowser._urlbarSelectionStart,
- newBrowser._urlbarSelectionEnd
- );
+ gURLBar.restoreSelectionStateForBrowser(newBrowser);
}
};
@@ -1656,6 +1693,22 @@
_dataURLRegEx: /^data:[^,]+;base64,/i,
+ // Regex to test if a string (potential tab label) consists of only non-
+ // printable characters. We consider Unicode categories Separator
+ // (spaces & line-breaks) and Other (control chars, private use, non-
+ // character codepoints) to be unprintable, along with a few specific
+ // characters whose expected rendering is blank:
+ // U+2800 BRAILLE PATTERN BLANK (category So)
+ // U+115F HANGUL CHOSEONG FILLER (category Lo)
+ // U+1160 HANGUL JUNGSEONG FILLER (category Lo)
+ // U+3164 HANGUL FILLER (category Lo)
+ // U+FFA0 HALFWIDTH HANGUL FILLER (category Lo)
+ // We also ignore combining marks, as in the absence of a printable base
+ // character they are unlikely to be usefully rendered, and may well be
+ // clipped away entirely.
+ _nonPrintingRegEx:
+ /^[\p{Z}\p{C}\p{M}\u{115f}\u{1160}\u{2800}\u{3164}\u{ffa0}]*$/u,
+
setTabTitle(aTab) {
var browser = this.getBrowserForTab(aTab);
var title = browser.contentTitle;
@@ -1676,6 +1729,16 @@
}
let isURL = false;
+
+ // Trim leading and trailing whitespace from the title.
+ title = title.trim();
+
+ // If the title contains only non-printing characters (or only combining
+ // marks, but no base character for them), we won't use it.
+ if (this._nonPrintingRegEx.test(title)) {
+ title = "";
+ }
+
let isContentTitle = !!title;
if (!title) {
// See if we can use the URI as the title.
@@ -2097,18 +2160,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",
@@ -2176,16 +2227,12 @@
b.setAttribute("name", name);
}
- let notificationbox = document.createXULElement("notificationbox");
- notificationbox.setAttribute("notificationside", "top");
-
let stack = document.createXULElement("stack");
stack.className = "browserStack";
stack.appendChild(b);
let browserContainer = document.createXULElement("vbox");
browserContainer.className = "browserContainer";
- browserContainer.appendChild(notificationbox);
browserContainer.appendChild(stack);
let browserSidebarContainer = document.createXULElement("hbox");
@@ -2682,8 +2729,6 @@
animate,
userContextId,
openerTab,
- createLazyBrowser,
- skipAnimation,
pinned,
noInitialLabel,
skipBackgroundNotify,
@@ -2849,8 +2894,6 @@
uriString,
userContextId,
openerTab,
- createLazyBrowser,
- skipAnimation,
pinned,
noInitialLabel,
skipBackgroundNotify,
@@ -3306,6 +3349,24 @@
return true;
}
+ const shownDupeDialogPref =
+ "browser.tabs.haveShownCloseAllDuplicateTabsWarning";
+ if (
+ aCloseTabs == this.closingTabsEnum.ALL_DUPLICATES &&
+ !Services.prefs.getBoolPref(shownDupeDialogPref, false)
+ ) {
+ // The first time a user closes all duplicate tabs, tell them what will
+ // happen and give them a chance to back away.
+ Services.prefs.setBoolPref(shownDupeDialogPref, true);
+
+ window.focus();
+ const [title, text] = this.tabLocalization.formatValuesSync([
+ { id: "tabbrowser-confirm-close-duplicate-tabs-title" },
+ { id: "tabbrowser-confirm-close-duplicate-tabs-text" },
+ ]);
+ return Services.prompt.confirm(window, title, text);
+ }
+
const pref =
aCloseTabs == this.closingTabsEnum.ALL
? "browser.tabs.warnOnClose"
@@ -3367,7 +3428,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";
@@ -3545,6 +3606,42 @@
return tabsToEnd;
},
+ removeDuplicateTabs(aTab) {
+ this._removeDuplicateTabs(
+ aTab,
+ this.getDuplicateTabsToClose(aTab),
+ this.closingTabsEnum.DUPLICATES
+ );
+ },
+
+ _removeDuplicateTabs(aConfirmationAnchor, tabs, aCloseTabs) {
+ if (!tabs.length) {
+ return;
+ }
+
+ if (!this.warnAboutClosingTabs(tabs.length, aCloseTabs)) {
+ return;
+ }
+
+ this.removeTabs(tabs);
+ ConfirmationHint.show(
+ aConfirmationAnchor,
+ "confirmation-hint-duplicate-tabs-closed",
+ { l10nArgs: { tabCount: tabs.length } }
+ );
+ },
+
+ removeAllDuplicateTabs() {
+ // I would like to have the caller provide this target,
+ // but the caller lives in a different document.
+ let alltabsButton = document.getElementById("alltabs-button");
+ this._removeDuplicateTabs(
+ alltabsButton,
+ this.getAllDuplicateTabsToClose(),
+ this.closingTabsEnum.ALL_DUPLICATES
+ );
+ },
+
/**
* In a multi-select context, the tabs (except pinned tabs) that are located to the
* left of the leftmost selected tab will be removed.
@@ -3674,6 +3771,8 @@
tabs,
{
animate,
+ // See bug 1883051
+ // eslint-disable-next-line no-unused-vars
suppressWarnAboutClosingWindow,
skipPermitUnload,
skipRemoves,
@@ -4390,6 +4489,56 @@
);
}
},
+ /**
+ * Closes tabs within the browser that match a given list of nsURIs. Returns
+ * any nsURIs that could not be closed successfully. This does not close any
+ * tabs that have a beforeUnload prompt
+ *
+ * @param {nsURI[]} urisToClose
+ * The set of uris to remove.
+ * @returns {nsURI[]}
+ * the nsURIs that weren't found in this browser
+ */
+ async closeTabsByURI(urisToClose) {
+ let remainingURIsToClose = [...urisToClose];
+ let tabsToRemove = [];
+ for (let tab of this.tabs) {
+ let currentURI = tab.linkedBrowser.currentURI;
+ // Find any URI that matches the current tab's URI
+ const matchedIndex = remainingURIsToClose.findIndex(uriToClose =>
+ uriToClose.equals(currentURI)
+ );
+
+ if (matchedIndex > -1) {
+ tabsToRemove.push(tab);
+ remainingURIsToClose.splice(matchedIndex, 1); // Remove the matched URI
+ }
+ }
+
+ if (tabsToRemove.length) {
+ const { beforeUnloadComplete, lastToClose } = this._startRemoveTabs(
+ tabsToRemove,
+ {
+ animate: false,
+ suppressWarnAboutClosingWindow: true,
+ skipPermitUnload: false,
+ skipRemoves: false,
+ skipSessionStore: false,
+ }
+ );
+
+ // Wait for the beforeUnload handlers to complete.
+ await beforeUnloadComplete;
+
+ // _startRemoveTabs doesn't close the last tab in the window
+ // for this use case, we simply close it
+ if (lastToClose) {
+ this.removeTab(lastToClose);
+ }
+ }
+ // If we still have uris, that means we couldn't find them in this window instance
+ return remainingURIsToClose;
+ },
/**
* Handles opening a new tab with mouse middleclick.
@@ -4406,7 +4555,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
@@ -5719,6 +5868,20 @@
}
},
+ getTabPids(tab) {
+ if (!tab.linkedBrowser) {
+ return [];
+ }
+
+ // Get the PIDs of the content process and remote subframe processes
+ let [contentPid, ...framePids] = E10SUtils.getBrowserPids(
+ tab.linkedBrowser,
+ gFissionBrowser
+ );
+ let pids = contentPid ? [contentPid] : [];
+ return pids.concat(framePids.sort());
+ },
+
getTabTooltip(tab, includeLabel = true) {
let labelArray = [];
if (includeLabel) {
@@ -5730,24 +5893,14 @@
false
)
) {
- if (tab.linkedBrowser) {
- // Show the PIDs of the content process and remote subframe processes.
- let [contentPid, ...framePids] = E10SUtils.getBrowserPids(
- tab.linkedBrowser,
- gFissionBrowser
- );
- if (contentPid) {
- if (framePids && framePids.length) {
- labelArray.push(
- `(pids ${contentPid}, ${framePids.sort().join(", ")})`
- );
- } else {
- labelArray.push(`(pid ${contentPid})`);
- }
- }
- if (tab.linkedBrowser.docShellIsActive) {
- labelArray.push("[A]");
- }
+ const pids = this.getTabPids(tab);
+ if (pids.length) {
+ let pidLabel = pids.length > 1 ? "pids" : "pid";
+ labelArray.push(`(${pidLabel} ${pids.join(", ")})`);
+ }
+
+ if (tab.linkedBrowser.docShellIsActive) {
+ labelArray.push("[A]");
}
}
@@ -5810,6 +5963,13 @@
tooltip.label = "";
document.l10n.setAttributes(tooltip, l10nId, l10nArgs);
} else {
+ // Prevent the tooltip from appearing if card preview is enabled, but
+ // only if the user is not hovering over the media play icon or the
+ // close button
+ if (this._showTabCardPreview) {
+ event.preventDefault();
+ return;
+ }
tooltip.label = this.getTabTooltip(tab, true);
}
},
@@ -5849,7 +6009,7 @@
}
},
- observe(aSubject, aTopic, aData) {
+ observe(aSubject, aTopic) {
switch (aTopic) {
case "contextual-identity-updated": {
let identity = aSubject.wrappedJSObject;
@@ -6105,12 +6265,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 +6507,7 @@
let oldUserTypedValue = browser.userTypedValue;
let hadStartedLoad = browser.didStartLoadSinceLastUserTyping();
- let didChange = didChangeEvent => {
+ let didChange = () => {
browser.userTypedValue = oldUserTypedValue;
if (hadStartedLoad) {
browser.urlbarChangeTracker.startedLoad();
@@ -7461,7 +7616,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(
@@ -7593,9 +7748,8 @@ var TabContextMenu = {
tabsToMove[0] == visibleTabs[gBrowser._numPinnedTabs];
contextMoveTabToStart.disabled = isFirstTab && allSelectedTabsAdjacent;
- if (this.contextTab.hasAttribute("customizemode")) {
- document.getElementById("context_openTabInWindow").disabled = true;
- }
+ document.getElementById("context_openTabInWindow").disabled =
+ this.contextTab.hasAttribute("customizemode");
// Only one of "Duplicate Tab"/"Duplicate Tabs" should be visible.
document.getElementById("context_duplicateTab").hidden =
@@ -7629,6 +7783,17 @@ var TabContextMenu = {
.getElementById("context_closeTab")
.setAttribute("data-l10n-args", tabCountInfo);
+ let closeDuplicateEnabled = Services.prefs.getBoolPref(
+ "browser.tabs.context.close-duplicate.enabled"
+ );
+ let closeDuplicateTabsItem = document.getElementById(
+ "context_closeDuplicateTabs"
+ );
+ closeDuplicateTabsItem.hidden = !closeDuplicateEnabled;
+ closeDuplicateTabsItem.disabled =
+ !closeDuplicateEnabled ||
+ !gBrowser.getDuplicateTabsToClose(this.contextTab).length;
+
// Disable "Close Multiple Tabs" if all sub menuitems are disabled
document.getElementById("context_closeTabOptions").disabled =
closeTabsToTheStartItem.disabled &&
@@ -7785,7 +7950,7 @@ var TabContextMenu = {
}
},
- closeContextTabs(event) {
+ closeContextTabs() {
if (this.contextTab.multiselected) {
gBrowser.removeMultiSelectedTabs();
} else {
diff --git a/browser/base/content/test/about/browser.toml b/browser/base/content/test/about/browser.toml
index 98961200a0..2c6dafb4dd 100644
--- a/browser/base/content/test/about/browser.toml
+++ b/browser/base/content/test/about/browser.toml
@@ -66,7 +66,6 @@ support-files = [
["browser_aboutNewTab_bookmarksToolbar.js"]
["browser_aboutNewTab_bookmarksToolbarEmpty.js"]
-fail-if = ["a11y_checks"] # Bug 1854233 text-link may not be focusable
skip-if = ["tsan"] # Bug 1676326, highly frequent on TSan
["browser_aboutNewTab_bookmarksToolbarNewWindow.js"]
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/about/browser_aboutNetError_csp_iframe.js b/browser/base/content/test/about/browser_aboutNetError_csp_iframe.js
index c8028a4cf4..d245d0cd3c 100644
--- a/browser/base/content/test/about/browser_aboutNetError_csp_iframe.js
+++ b/browser/base/content/test/about/browser_aboutNetError_csp_iframe.js
@@ -8,6 +8,10 @@ const BLOCKED_PAGE =
"http://example.org:8000/browser/browser/base/content/test/about/csp_iframe.sjs";
add_task(async function test_csp() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.xfocsp.hideOpenInNewWindow", false]],
+ });
+
let { iframePageTab, blockedPageTab } = await setupPage(
"iframe_page_csp.html",
BLOCKED_PAGE
diff --git a/browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js b/browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js
index f5fd240643..2373bd8b50 100644
--- a/browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js
+++ b/browser/base/content/test/about/browser_aboutNetError_xfo_iframe.js
@@ -8,6 +8,10 @@ const BLOCKED_PAGE =
"http://example.org:8000/browser/browser/base/content/test/about/xfo_iframe.sjs";
add_task(async function test_xfo_iframe() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.xfocsp.hideOpenInNewWindow", false]],
+ });
+
let { iframePageTab, blockedPageTab } = await setupPage(
"iframe_page_xfo.html",
BLOCKED_PAGE
diff --git a/browser/base/content/test/alerts/browser.toml b/browser/base/content/test/alerts/browser.toml
index d0d56f7392..aaca2ba7dc 100644
--- a/browser/base/content/test/alerts/browser.toml
+++ b/browser/base/content/test/alerts/browser.toml
@@ -19,10 +19,6 @@ skip-if = ["os == 'win'"] # Bug 1411118
https_first_disabled = true
skip-if = ["os == 'win'"] # Bug 1411118
-["browser_notification_replace.js"]
-https_first_disabled = true
-skip-if = ["os == 'win'"] # Bug 1422928
-
["browser_notification_tab_switching.js"]
https_first_disabled = true
skip-if = ["os == 'win'"] # Bug 1243263
diff --git a/browser/base/content/test/alerts/browser_notification_close.js b/browser/base/content/test/alerts/browser_notification_close.js
index 7568f1cc2d..3fd50bed5b 100644
--- a/browser/base/content/test/alerts/browser_notification_close.js
+++ b/browser/base/content/test/alerts/browser_notification_close.js
@@ -21,17 +21,16 @@ add_task(async function test_notificationClose() {
Services.prefs.setBoolPref("alerts.showFavicons", true);
await PlacesTestUtils.addVisits(notificationURI);
- let faviconURI = await new Promise(resolve => {
- let uri = makeURI(
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"
- );
- PlacesUtils.favicons.setAndFetchFaviconForPage(
+ let dataURL = makeURI(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"
+ );
+ await new Promise(resolve => {
+ PlacesUtils.favicons.setFaviconForPage(
notificationURI,
- uri,
- true,
- PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
- uriResult => resolve(uriResult),
- Services.scriptSecurityManager.getSystemPrincipal()
+ dataURL,
+ dataURL,
+ null,
+ resolve
);
});
@@ -67,11 +66,7 @@ add_task(async function test_notificationClose() {
"Body text of notification should be present"
);
let alertIcon = alertWindow.document.getElementById("alertIcon");
- is(
- alertIcon.src,
- faviconURI.spec,
- "Icon of notification should be present"
- );
+ is(alertIcon.src, dataURL.spec, "Icon of notification should be present");
let alertCloseButton = alertWindow.document.querySelector(".close-icon");
is(alertCloseButton.localName, "toolbarbutton", "close button found");
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/browser_notification_replace.js b/browser/base/content/test/alerts/browser_notification_replace.js
deleted file mode 100644
index 9c72e90ab1..0000000000
--- a/browser/base/content/test/alerts/browser_notification_replace.js
+++ /dev/null
@@ -1,66 +0,0 @@
-"use strict";
-
-let notificationURL =
- // eslint-disable-next-line @microsoft/sdl/no-insecure-url
- "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
-
-add_task(async function test_notificationReplace() {
- await addNotificationPermission(notificationURL);
-
- await BrowserTestUtils.withNewTab(
- {
- gBrowser,
- url: notificationURL,
- },
- async function dummyTabTask(aBrowser) {
- await SpecialPowers.spawn(aBrowser, [], async function () {
- let win = content.window.wrappedJSObject;
- let notification = win.showNotification1();
- let promiseCloseEvent = ContentTaskUtils.waitForEvent(
- notification,
- "close"
- );
-
- let showEvent = await ContentTaskUtils.waitForEvent(
- notification,
- "show"
- );
- Assert.equal(
- showEvent.target.body,
- "Test body 1",
- "Showed tagged notification"
- );
-
- let newNotification = win.showNotification2();
- let newShowEvent = await ContentTaskUtils.waitForEvent(
- newNotification,
- "show"
- );
- Assert.equal(
- newShowEvent.target.body,
- "Test body 2",
- "Showed new notification with same tag"
- );
-
- let closeEvent = await promiseCloseEvent;
- Assert.equal(
- closeEvent.target.body,
- "Test body 1",
- "Closed previous tagged notification"
- );
-
- let promiseNewCloseEvent = ContentTaskUtils.waitForEvent(
- newNotification,
- "close"
- );
- newNotification.close();
- let newCloseEvent = await promiseNewCloseEvent;
- Assert.equal(
- newCloseEvent.target.body,
- "Test body 2",
- "Closed new notification"
- );
- });
- }
- );
-});
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..c7af1bc437 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"]
@@ -99,3 +101,5 @@ support-files = [
"test_view_image_inline_svg.html",
]
skip-if = ["os == 'linux' && socketprocess_networking"]
+
+["browser_contextmenu_cross_boundary_selection.js"]
diff --git a/browser/base/content/test/contextMenu/browser_contextmenu.js b/browser/base/content/test/contextMenu/browser_contextmenu.js
index ebeb4bdb04..d8dc0ab5e0 100644
--- a/browser/base/content/test/contextMenu/browser_contextmenu.js
+++ b/browser/base/content/test/contextMenu/browser_contextmenu.js
@@ -41,6 +41,10 @@ let hasContainers =
Services.prefs.getBoolPref("privacy.userContext.enabled") &&
ContextualIdentityService.getPublicIdentities().length;
+const hasSelectTranslations =
+ Services.prefs.getBoolPref("browser.translations.enable") &&
+ Services.prefs.getBoolPref("browser.translations.select.enable");
+
const example_base =
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
"http://example.com/browser/browser/base/content/test/contextMenu/";
@@ -112,6 +116,7 @@ add_task(async function test_xul_text_link_label() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
// Clean up so won't affect HTML element test cases.
@@ -137,7 +142,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(
@@ -204,6 +209,7 @@ const kLinkItems = [
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
];
add_task(async function test_link() {
@@ -234,6 +240,7 @@ add_task(async function test_mailto() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
});
@@ -247,6 +254,7 @@ add_task(async function test_tel() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
});
@@ -273,6 +281,10 @@ add_task(async function test_image() {
null,
"context-setDesktopBackground",
true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
],
{
onContextMenuShown() {
@@ -356,6 +368,10 @@ add_task(async function test_video_ok() {
true,
"context-sendvideo",
true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
]);
await SpecialPowers.popPrefEnv();
@@ -404,6 +420,10 @@ add_task(async function test_video_ok() {
true,
"context-sendvideo",
true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
]);
await SpecialPowers.popPrefEnv();
@@ -490,6 +510,10 @@ add_task(async function test_video_bad() {
true,
"context-sendvideo",
true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
]);
await SpecialPowers.popPrefEnv();
@@ -538,6 +562,10 @@ add_task(async function test_video_bad() {
true,
"context-sendvideo",
true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
]);
await SpecialPowers.popPrefEnv();
@@ -588,6 +616,10 @@ add_task(async function test_video_bad2() {
false,
"context-sendvideo",
false,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
]);
await SpecialPowers.popPrefEnv();
@@ -636,6 +668,10 @@ add_task(async function test_video_bad2() {
false,
"context-sendvideo",
false,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
]);
await SpecialPowers.popPrefEnv();
@@ -767,6 +803,10 @@ add_task(async function test_video_in_iframe() {
true,
"---",
null,
+ "context-take-frame-screenshot",
+ true,
+ "---",
+ null,
"context-viewframeinfo",
true,
]),
@@ -846,6 +886,10 @@ add_task(async function test_video_in_iframe() {
true,
"---",
null,
+ "context-take-frame-screenshot",
+ true,
+ "---",
+ null,
"context-viewframeinfo",
true,
]),
@@ -967,6 +1011,10 @@ add_task(async function test_image_in_iframe() {
true,
"---",
null,
+ "context-take-frame-screenshot",
+ true,
+ "---",
+ null,
"context-viewframeinfo",
true,
]),
@@ -1336,6 +1384,7 @@ add_task(async function test_select_text() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
"---",
null,
"context-viewpartialsource-selection",
@@ -1369,6 +1418,9 @@ add_task(async function test_select_text_search_service_not_initialized() {
null,
"context-take-screenshot",
true,
+ ...(hasSelectTranslations
+ ? ["---", null, "context-translate-selection", true]
+ : []),
"---",
null,
"context-viewpartialsource-selection",
@@ -1423,6 +1475,7 @@ add_task(async function test_select_text_link() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
"---",
null,
"context-viewpartialsource-selection",
@@ -1490,6 +1543,9 @@ add_task(async function test_imagelink() {
null,
"context-setDesktopBackground",
true,
+ ...(hasSelectTranslations
+ ? ["---", null, "context-translate-selection", true]
+ : []),
]);
});
@@ -1591,6 +1647,10 @@ add_task(async function test_longdesc() {
null,
"context-setDesktopBackground",
true,
+ "---",
+ null,
+ "context-take-screenshot",
+ true,
]);
});
@@ -1682,6 +1742,7 @@ add_task(async function test_svg_link() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
await test_contextmenu("#svg-with-link2 > a", [
@@ -1711,6 +1772,7 @@ add_task(async function test_svg_link() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
await test_contextmenu("#svg-with-link3 > a", [
@@ -1740,6 +1802,7 @@ add_task(async function test_svg_link() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
});
@@ -1771,6 +1834,7 @@ add_task(async function test_svg_relative_link() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
await test_contextmenu("#svg-with-relative-link2 > a", [
@@ -1800,6 +1864,7 @@ add_task(async function test_svg_relative_link() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
await test_contextmenu("#svg-with-relative-link3 > a", [
@@ -1829,6 +1894,7 @@ add_task(async function test_svg_relative_link() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
});
@@ -1898,6 +1964,7 @@ add_task(async function test_background_image() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
// Don't show image related context menu commands when there is a selection
@@ -1921,6 +1988,7 @@ add_task(async function test_background_image() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
"---",
null,
"context-viewpartialsource-selection",
@@ -1989,6 +2057,7 @@ add_task(async function test_strip_on_share_on_secure_about_page() {
true,
"context-searchselect-private",
true,
+ ...(hasSelectTranslations ? ["context-translate-selection", true] : []),
]);
// Clean up
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_cross_boundary_selection.js b/browser/base/content/test/contextMenu/browser_contextmenu_cross_boundary_selection.js
new file mode 100644
index 0000000000..3137a1e136
--- /dev/null
+++ b/browser/base/content/test/contextMenu/browser_contextmenu_cross_boundary_selection.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PAGE = `
+ data:text/html,
+ <div>OuterText<div>
+ <div id="host">
+ <template shadowrootmode="open">
+ <span id="innerText">InnerText</span>
+ </template>
+ </div>
+ `;
+/**
+ * Tests that right click on a cross boundary selection shows the context menu
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.shadowdom.selection_across_boundary.enabled", true]],
+ });
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async function (browser) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+
+ await SpecialPowers.spawn(browser, [], () => {
+ let host = content.document.getElementById("host");
+ content.getSelection().setBaseAndExtent(
+ content.document.body,
+ 0,
+ host.shadowRoot.getElementById("innerText").firstChild,
+ 3 // Only select the first three characters of the inner text
+ );
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "div", // Click on the div for OuterText
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ browser
+ );
+
+ await awaitPopupShown;
+
+ const allVisibleMenuItems = Array.from(contextMenu.children)
+ .filter(elem => {
+ return !elem.hidden;
+ })
+ .map(elem => elem.id);
+
+ ok(
+ allVisibleMenuItems.includes("context-copy"),
+ "copy button should exist"
+ );
+
+ contextMenu.hidePopup();
+ await awaitPopupHidden;
+ }
+ );
+});
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_link.js b/browser/base/content/test/contextMenu/browser_strip_on_share_link.js
index 435b1aa0ff..144076cfb2 100644
--- a/browser/base/content/test/contextMenu/browser_strip_on_share_link.js
+++ b/browser/base/content/test/contextMenu/browser_strip_on_share_link.js
@@ -97,6 +97,18 @@ add_task(async function testQueryParamIsNotStrippedForWrongSiteSpecific() {
});
});
+// Ensuring clean copy works with magnet links. We don't strip anything but copying the original URI should still work.
+add_task(async function testMagneticLinks() {
+ let originalUrl = "magnet:?xt=urn:btih:somesha1hash";
+ let shortenedUrl = "magnet:?xt=urn:btih:somesha1hash";
+ await testStripOnShare({
+ selectWholeUrl: true,
+ validUrl: originalUrl,
+ strippedUrl: shortenedUrl,
+ useTestList: true,
+ });
+});
+
/**
* 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.
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_documentnavigation.js b/browser/base/content/test/general/browser_documentnavigation.js
index 880db6110f..b68e6ec3f0 100644
--- a/browser/base/content/test/general/browser_documentnavigation.js
+++ b/browser/base/content/test/general/browser_documentnavigation.js
@@ -229,7 +229,7 @@ add_task(async function () {
let sidebar = document.getElementById("sidebar");
let loadPromise = BrowserTestUtils.waitForEvent(sidebar, "load", true);
- SidebarUI.toggle("viewBookmarksSidebar");
+ SidebarController.toggle("viewBookmarksSidebar");
await loadPromise;
gURLBar.focus();
@@ -278,7 +278,7 @@ add_task(async function () {
"back focus with sidebar urlbar"
);
- SidebarUI.toggle("viewBookmarksSidebar");
+ SidebarController.toggle("viewBookmarksSidebar");
});
// Navigate when the downloads panel is open
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
deleted file mode 100644
index ac7ece3519..0000000000
--- a/browser/base/content/test/general/video.ogg
+++ /dev/null
Binary files differ
diff --git a/browser/base/content/test/general/video.webm b/browser/base/content/test/general/video.webm
new file mode 100644
index 0000000000..0ca38d3cf0
--- /dev/null
+++ b/browser/base/content/test/general/video.webm
Binary files differ
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
deleted file mode 100644
index 093158432a..0000000000
--- a/browser/base/content/test/general/web_video1.ogv
+++ /dev/null
Binary files differ
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
new file mode 100644
index 0000000000..2c9d7dad8d
--- /dev/null
+++ b/browser/base/content/test/general/web_video1.webm
Binary files differ
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..9ad9a8dde8 100644
--- a/browser/base/content/test/performance/browser_preferences_usage.js
+++ b/browser/base/content/test/performance/browser_preferences_usage.js
@@ -70,13 +70,6 @@ function checkPrefGetters(stats, max, knownProblematicPrefs = {}) {
}
}
- // This pref will be accessed by mozJSComponentLoader when loading modules,
- // which fails TV runs since they run the test multiple times without restarting.
- // We just ignore this pref, since it's for testing only anyway.
- if (knownProblematicPrefs["browser.startup.record"]) {
- delete knownProblematicPrefs["browser.startup.record"];
- }
-
let unusedPrefs = Object.keys(knownProblematicPrefs);
is(
unusedPrefs.length,
@@ -104,18 +97,9 @@ add_task(async function startup() {
let max = 40;
let knownProblematicPrefs = {
- "browser.startup.record": {
- // This pref is accessed in Nighly and debug builds only.
- min: 200,
- max: 400,
- },
"network.loadinfo.skip_type_assertion": {
// This is accessed in debug only.
},
- "chrome.override_package.global": {
- min: 0,
- max: 50,
- },
};
let startupRecorder =
@@ -135,9 +119,6 @@ add_task(async function open_10_tabs() {
const max = 4 * DEFAULT_PROCESS_COUNT;
let knownProblematicPrefs = {
- "browser.startup.record": {
- max: 20,
- },
"browser.tabs.remote.logSwitchTiming": {
max: 35,
},
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..3c652b26a2 100644
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_security_delay.js
@@ -44,22 +44,22 @@ add_setup(async function () {
});
});
+/**
+ * The security delay calculation in PopupNotification.sys.mjs is dependent on
+ * the monotonically increasing value of performance.now. This timestamp is
+ * not relative to a fixed date, but to runtime.
+ * We need to wait for the value performance.now() to be larger than the
+ * security delay in order to observe the bug. Only then does the
+ * timeSinceShown check in PopupNotifications.sys.mjs lead to a timeSinceShown
+ * value that is unconditionally greater than lazy.buttonDelay for
+ * notification.timeShown = null = 0.
+ * See: https://searchfox.org/mozilla-central/rev/f32d5f3949a3f4f185122142b29f2e3ab776836e/toolkit/modules/PopupNotifications.sys.mjs#1870-1872
+ *
+ * When running in automation as part of a larger test suite performance.now()
+ * should usually be already sufficiently high in which case this check should
+ * directly resolve.
+ */
async function ensureSecurityDelayReady() {
- /**
- * The security delay calculation in PopupNotification.sys.mjs is dependent on
- * the monotonically increasing value of performance.now. This timestamp is
- * not relative to a fixed date, but to runtime.
- * We need to wait for the value performance.now() to be larger than the
- * security delay in order to observe the bug. Only then does the
- * timeSinceShown check in PopupNotifications.sys.mjs lead to a timeSinceShown
- * value that is unconditionally greater than lazy.buttonDelay for
- * notification.timeShown = null = 0.
- * See: https://searchfox.org/mozilla-central/rev/f32d5f3949a3f4f185122142b29f2e3ab776836e/toolkit/modules/PopupNotifications.sys.mjs#1870-1872
- *
- * When running in automation as part of a larger test suite performance.now()
- * should usually be already sufficiently high in which case this check should
- * directly resolve.
- */
await TestUtils.waitForCondition(
() => performance.now() > TEST_SECURITY_DELAY,
"Wait for performance.now() > SECURITY_DELAY",
@@ -69,73 +69,73 @@ async function ensureSecurityDelayReady() {
}
/**
- * Tests that when we show a second notification while the panel is open the
- * timeShown attribute is correctly set and the security delay is enforced
- * properly.
+ * Test helper for security delay tests which performs the following steps:
+ * 1. Shows a test notification.
+ * 2. Waits for the notification panel to be shown and checks that the initial
+ * security delay blocks clicks.
+ * 3. Waits for the security delay to expire. Clicks should now be allowed.
+ * 4. Executes the provided onSecurityDelayExpired function. This function
+ * should renew the security delay.
+ * 5. Tests that the security delay was renewed.
+ * 6. Ensures that after the security delay the notification can be closed.
+ *
+ * @param {*} options
+ * @param {function} options.onSecurityDelayExpired - Function to run after the
+ * security delay has expired. This function should trigger a renew of the
+ * security delay.
+ * @param {function} options.cleanupFn - Optional cleanup function to run after
+ * the test has completed.
+ * @returns {Promise<void>} - Resolves when the test has completed.
*/
-add_task(async function test_timeShownMultipleNotifications() {
+async function runPopupNotificationSecurityDelayTest({
+ onSecurityDelayExpired,
+ cleanupFn = () => {},
+}) {
await ensureSecurityDelayReady();
- ok(
- !PopupNotifications.isPanelOpen,
- "PopupNotification panel should not be open initially."
- );
-
- info("Open the first notification.");
+ info("Open a notification.");
let popupShownPromise = waitForNotificationPanel();
showNotification();
await popupShownPromise;
ok(
PopupNotifications.isPanelOpen,
- "PopupNotification should be open after first show call."
+ "PopupNotification should be open after show call."
);
- is(
- PopupNotifications._currentNotifications.length,
- 1,
- "There should only be one notification"
+ // Test that the initial security delay works.
+ info(
+ "Trigger main action via button click during the initial security delay."
);
+ triggerMainCommand(PopupNotifications.panel);
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
let notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
- is(notification?.id, "foo", "There should be a notification with id foo");
- ok(notification.timeShown, "The notification should have timeShown set");
-
- info(
- "Call show again with the same notification id while the PopupNotification panel is still open."
- );
- showNotification();
ok(
- PopupNotifications.isPanelOpen,
- "PopupNotification should still open after second show call."
- );
- notification = PopupNotifications.getNotification(
- "foo",
- gBrowser.selectedBrowser
- );
- is(
- PopupNotifications._currentNotifications.length,
- 1,
- "There should still only be one notification"
+ notification,
+ "Notification should still be open because we clicked during the security delay."
);
+ // If the notification is no longer shown (test failure) skip the remaining
+ // checks.
+ if (!notification) {
+ await cleanupFn();
+ return;
+ }
- is(
- notification?.id,
- "foo",
- "There should still be a notification with id foo"
+ info("Wait for security delay to expire.");
+ await new Promise(resolve =>
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(resolve, TEST_SECURITY_DELAY + 500)
);
- ok(notification.timeShown, "The notification should have timeShown set");
- let notificationHiddenPromise = waitForNotificationPanelHidden();
-
- info("Trigger main action via button click during security delay");
-
- // Wait for a tick of the event loop to ensure the button we're clicking
- // has been slotted into moz-button-group
- await new Promise(resolve => setTimeout(resolve, 0));
+ info("Run test specific actions which restarts the security delay.");
+ await onSecurityDelayExpired();
+ info("Trigger main action via button click during the new security delay.");
triggerMainCommand(PopupNotifications.panel);
await new Promise(resolve => setTimeout(resolve, 0));
@@ -149,10 +149,10 @@ add_task(async function test_timeShownMultipleNotifications() {
notification,
"Notification should still be open because we clicked during the security delay."
);
-
// If the notification is no longer shown (test failure) skip the remaining
// checks.
if (!notification) {
+ await cleanupFn();
return;
}
@@ -163,6 +163,7 @@ add_task(async function test_timeShownMultipleNotifications() {
notification.timeShown = performance.now() - fakeTimeShown;
info("Trigger main action via button click outside security delay");
+ let notificationHiddenPromise = waitForNotificationPanelHidden();
triggerMainCommand(PopupNotifications.panel);
info("Wait for panel to be hidden.");
@@ -170,15 +171,19 @@ add_task(async function test_timeShownMultipleNotifications() {
ok(
!PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
- "Should not longer see the notification."
+ "Should no longer see the notification."
);
-});
+
+ info("Cleanup.");
+ await cleanupFn();
+}
/**
- * Tests that when we reshow a notification after a tab switch the timeShown
- * attribute is correctly reset and the security delay is enforced.
+ * Tests that when we show a second notification while the panel is open the
+ * timeShown attribute is correctly set and the security delay is enforced
+ * properly.
*/
-add_task(async function test_notificationReshowTabSwitch() {
+add_task(async function test_timeShownMultipleNotifications() {
await ensureSecurityDelayReady();
ok(
@@ -195,6 +200,12 @@ add_task(async function test_notificationReshowTabSwitch() {
"PopupNotification should be open after first show call."
);
+ is(
+ PopupNotifications._currentNotifications.length,
+ 1,
+ "There should only be one notification"
+ );
+
let notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
@@ -202,69 +213,39 @@ add_task(async function test_notificationReshowTabSwitch() {
is(notification?.id, "foo", "There should be a notification with id foo");
ok(notification.timeShown, "The notification should have timeShown set");
- info("Trigger main action via button click during security delay");
- triggerMainCommand(PopupNotifications.panel);
-
- await new Promise(resolve => setTimeout(resolve, 0));
-
- ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
- notification = PopupNotifications.getNotification(
- "foo",
- gBrowser.selectedBrowser
- );
- ok(
- notification,
- "Notification should still be open because we clicked during the security delay."
- );
-
- // If the notification is no longer shown (test failure) skip the remaining
- // checks.
- if (!notification) {
- return;
- }
-
- let panelHiddenPromise = waitForNotificationPanelHidden();
- let panelShownPromise;
-
- info("Open a new tab which hides the notification panel.");
- await BrowserTestUtils.withNewTab("https://example.com", async browser => {
- info("Wait for panel to be hidden by tab switch.");
- await panelHiddenPromise;
- info(
- "Keep the tab open until the security delay for the original notification show has expired."
- );
- await new Promise(resolve =>
- // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
- setTimeout(resolve, TEST_SECURITY_DELAY + 500)
- );
-
- panelShownPromise = waitForNotificationPanel();
- });
info(
- "Wait for the panel to show again after the tab close. We're showing the original tab again."
+ "Call show again with the same notification id while the PopupNotification panel is still open."
);
- await panelShownPromise;
-
+ showNotification();
ok(
PopupNotifications.isPanelOpen,
- "PopupNotification should be shown after tab close."
+ "PopupNotification should still open after second show call."
);
notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
is(
+ PopupNotifications._currentNotifications.length,
+ 1,
+ "There should still only be one notification"
+ );
+
+ is(
notification?.id,
"foo",
"There should still be a notification with id foo"
);
+ ok(notification.timeShown, "The notification should have timeShown set");
let notificationHiddenPromise = waitForNotificationPanelHidden();
- info(
- "Because we re-show the panel after tab close / switch the security delay should have reset."
- );
- info("Trigger main action via button click during the new security delay.");
+ info("Trigger main action via button click during security delay");
+
+ // Wait for a tick of the event loop to ensure the button we're clicking
+ // has been slotted into moz-button-group
+ await new Promise(resolve => setTimeout(resolve, 0));
+
triggerMainCommand(PopupNotifications.panel);
await new Promise(resolve => setTimeout(resolve, 0));
@@ -278,6 +259,7 @@ add_task(async function test_notificationReshowTabSwitch() {
notification,
"Notification should still be open because we clicked during the security delay."
);
+
// If the notification is no longer shown (test failure) skip the remaining
// checks.
if (!notification) {
@@ -298,109 +280,83 @@ add_task(async function test_notificationReshowTabSwitch() {
ok(
!PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
- "Should not longer see the notification."
+ "Should no longer see the notification."
);
});
/**
+ * Tests that when we reshow a notification after a tab switch the timeShown
+ * attribute is correctly reset and the security delay is enforced.
+ */
+add_task(async function test_notificationReshowTabSwitch() {
+ await runPopupNotificationSecurityDelayTest({
+ onSecurityDelayExpired: async () => {
+ let panelHiddenPromise = waitForNotificationPanelHidden();
+ let panelShownPromise;
+
+ info("Open a new tab which hides the notification panel.");
+ await BrowserTestUtils.withNewTab("https://example.com", async () => {
+ info("Wait for panel to be hidden by tab switch.");
+ await panelHiddenPromise;
+ panelShownPromise = waitForNotificationPanel();
+ });
+ info(
+ "Wait for the panel to show again after the tab close. We're showing the original tab again."
+ );
+ await panelShownPromise;
+
+ ok(
+ PopupNotifications.isPanelOpen,
+ "PopupNotification should be shown after tab close."
+ );
+ let notification = PopupNotifications.getNotification(
+ "foo",
+ gBrowser.selectedBrowser
+ );
+ is(
+ notification?.id,
+ "foo",
+ "There should still be a notification with id foo"
+ );
+
+ info(
+ "Because we re-show the panel after tab close / switch the security delay should have reset."
+ );
+ },
+ });
+});
+
+/**
* Tests that the security delay gets reset when a window is repositioned and
* the PopupNotifications panel position is updated.
*/
add_task(async function test_notificationWindowMove() {
- await ensureSecurityDelayReady();
-
- info("Open a notification.");
- let popupShownPromise = waitForNotificationPanel();
- showNotification();
- await popupShownPromise;
- ok(
- PopupNotifications.isPanelOpen,
- "PopupNotification should be open after show call."
- );
-
- // Test that the initial security delay works.
- info("Trigger main action via button click during the new security delay.");
- triggerMainCommand(PopupNotifications.panel);
-
- await new Promise(resolve => setTimeout(resolve, 0));
-
- ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
- let notification = PopupNotifications.getNotification(
- "foo",
- gBrowser.selectedBrowser
- );
- ok(
- notification,
- "Notification should still be open because we clicked during the security delay."
- );
- // If the notification is no longer shown (test failure) skip the remaining
- // checks.
- if (!notification) {
- return;
- }
-
- info("Wait for security delay to expire.");
- await new Promise(resolve =>
- // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
- setTimeout(resolve, TEST_SECURITY_DELAY + 500)
- );
-
- info("Reposition the window");
- // Remember original window position.
- let { screenX, screenY } = window;
-
- let promisePopupPositioned = BrowserTestUtils.waitForEvent(
- PopupNotifications.panel,
- "popuppositioned"
- );
-
- // Move the window.
- window.moveTo(200, 200);
-
- // Wait for the panel to reposition and the PopupNotifications listener to run.
- await promisePopupPositioned;
- await new Promise(resolve => setTimeout(resolve, 0));
-
- info("Trigger main action via button click during the new security delay.");
- triggerMainCommand(PopupNotifications.panel);
-
- await new Promise(resolve => setTimeout(resolve, 0));
-
- ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
- notification = PopupNotifications.getNotification(
- "foo",
- gBrowser.selectedBrowser
- );
- ok(
- notification,
- "Notification should still be open because we clicked during the security delay."
- );
- // If the notification is no longer shown (test failure) skip the remaining
- // checks.
- if (!notification) {
- return;
- }
-
- // Ensure that once the security delay has passed the notification can be
- // closed again.
- let fakeTimeShown = TEST_SECURITY_DELAY + 500;
- info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
- notification.timeShown = performance.now() - fakeTimeShown;
-
- info("Trigger main action via button click outside security delay");
- let notificationHiddenPromise = waitForNotificationPanelHidden();
- triggerMainCommand(PopupNotifications.panel);
-
- info("Wait for panel to be hidden.");
- await notificationHiddenPromise;
-
- ok(
- !PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
- "Should not longer see the notification."
- );
-
- // Reset window position
- window.moveTo(screenX, screenY);
+ let screenX, screenY;
+
+ await runPopupNotificationSecurityDelayTest({
+ onSecurityDelayExpired: async () => {
+ info("Reposition the window");
+ // Remember original window position.
+ screenX = window.screenX;
+ screenY = window.screenY;
+
+ let promisePopupPositioned = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popuppositioned"
+ );
+
+ // Move the window.
+ window.moveTo(200, 200);
+
+ // Wait for the panel to reposition and the PopupNotifications listener to run.
+ await promisePopupPositioned;
+ await new Promise(resolve => setTimeout(resolve, 0));
+ },
+ cleanupFn: async () => {
+ // Reset window position
+ window.moveTo(screenX, screenY);
+ },
+ });
});
/**
@@ -563,5 +519,49 @@ add_task(async function test_notificationDuringFullScreenTransition() {
info("Wait for full screen transition end.");
await promiseFullScreenTransitionEnd;
info("Full screen transition end");
+
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+/**
+ * Tests that the security delay gets extended when pointer lock is entered.
+ */
+add_task(async function test_notificationPointerLock() {
+ // We need a tab to enter pointer lock.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ await runPopupNotificationSecurityDelayTest({
+ onSecurityDelayExpired: async () => {
+ info("Enter pointer lock");
+ // Move focus to the browser to ensure pointer lock request succeeds.
+ gBrowser.selectedBrowser.focus();
+ let pointerLockEnterPromise = TestUtils.topicObserved(
+ "pointer-lock-entered"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ await content.document.body.requestPointerLock();
+ });
+
+ // Wait for pointer lock to be entered and the PopupNotifications listener to run.
+ await pointerLockEnterPromise;
+ await new Promise(resolve => setTimeout(resolve, 0));
+ },
+ cleanupFn: async () => {
+ // Cleanup.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+ await content.document.exitPointerLock();
+ });
+ await TestUtils.waitForCondition(
+ () => !window.PointerLock.isActive,
+ "Wait for pointer lock exit."
+ );
+ BrowserTestUtils.removeTab(tab);
+ },
});
});
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_adopt.js b/browser/base/content/test/sidebar/browser_sidebar_adopt.js
index 344a71cb9b..988ac1487f 100644
--- a/browser/base/content/test/sidebar/browser_sidebar_adopt.js
+++ b/browser/base/content/test/sidebar/browser_sidebar_adopt.js
@@ -5,7 +5,7 @@
* during the initial browser startup - but it would be hard to do with a mochitest. */
registerCleanupFunction(() => {
- SidebarUI.hide();
+ SidebarController.hide();
});
function failIfSidebarFocusedFires() {
@@ -26,7 +26,7 @@ add_task(async function testAdoptedTwoWindows() {
info("Ensure that sidebar state is adopted only from the opener");
let win1 = await BrowserTestUtils.openNewBrowserWindow();
- await win1.SidebarUI.show("viewBookmarksSidebar");
+ await win1.SidebarController.show("viewBookmarksSidebar");
await BrowserTestUtils.closeWindow(win1);
let win2 = await BrowserTestUtils.openNewBrowserWindow();
@@ -34,7 +34,7 @@ add_task(async function testAdoptedTwoWindows() {
!win2.document.getElementById("sidebar-button").hasAttribute("checked"),
"Sidebar button isn't checked"
);
- ok(!win2.SidebarUI.isOpen, "Sidebar is closed");
+ ok(!win2.SidebarController.isOpen, "Sidebar is closed");
await BrowserTestUtils.closeWindow(win2);
});
@@ -46,7 +46,7 @@ add_task(async function testEventsReceivedInMainWindow() {
let initialShown = BrowserTestUtils.waitForEvent(window, "SidebarShown");
let initialFocus = BrowserTestUtils.waitForEvent(window, "SidebarFocused");
- await SidebarUI.show("viewBookmarksSidebar");
+ await SidebarController.show("viewBookmarksSidebar");
await initialShown;
await initialFocus;
diff --git a/browser/base/content/test/sidebar/browser_sidebar_app_locale_changed.js b/browser/base/content/test/sidebar/browser_sidebar_app_locale_changed.js
index 5b07da9839..1e52895a7d 100644
--- a/browser/base/content/test/sidebar/browser_sidebar_app_locale_changed.js
+++ b/browser/base/content/test/sidebar/browser_sidebar_app_locale_changed.js
@@ -8,7 +8,7 @@
add_task(function cleanup() {
registerCleanupFunction(() => {
- SidebarUI.hide();
+ SidebarController.hide();
});
});
@@ -17,7 +17,7 @@ add_task(function cleanup() {
*/
async function testLiveReloading(sidebarName) {
info("Showing the sidebar " + sidebarName);
- await SidebarUI.show(sidebarName);
+ await SidebarController.show(sidebarName);
function getTreeChildren() {
const sidebarDoc =
@@ -44,7 +44,7 @@ async function testLiveReloading(sidebarName) {
);
info("Hiding the sidebar");
- SidebarUI.hide();
+ SidebarController.hide();
}
add_task(async function test_bookmarks_sidebar() {
diff --git a/browser/base/content/test/sidebar/browser_sidebar_keys.js b/browser/base/content/test/sidebar/browser_sidebar_keys.js
index f12d1cf5f7..61e4ce9737 100644
--- a/browser/base/content/test/sidebar/browser_sidebar_keys.js
+++ b/browser/base/content/test/sidebar/browser_sidebar_keys.js
@@ -11,11 +11,11 @@ async function testSidebarKeyToggle(key, options, expectedSidebarId) {
expectedSidebarId
);
EventUtils.synthesizeKey(key, options);
- Assert.ok(!SidebarUI.isOpen);
+ Assert.ok(!SidebarController.isOpen);
}
add_task(async function test_sidebar_keys() {
- registerCleanupFunction(() => SidebarUI.hide());
+ registerCleanupFunction(() => SidebarController.hide());
await testSidebarKeyToggle("b", { accelKey: true }, "viewBookmarksSidebar");
@@ -30,7 +30,7 @@ add_task(async function test_sidebar_in_customize_mode() {
let { CustomizableUI } = ChromeUtils.importESModule(
"resource:///modules/CustomizableUI.sys.mjs"
);
- registerCleanupFunction(() => SidebarUI.hide());
+ registerCleanupFunction(() => SidebarController.hide());
let placement = CustomizableUI.getPlacementOfWidget("sidebar-button");
if (!(placement?.area == CustomizableUI.AREA_NAVBAR)) {
@@ -55,7 +55,7 @@ add_task(async function test_sidebar_in_customize_mode() {
).a;
let promiseShown = BrowserTestUtils.waitForEvent(window, "SidebarShown");
- SidebarUI.show("viewBookmarksSidebar");
+ SidebarController.show("viewBookmarksSidebar");
await promiseShown;
Assert.greater(
@@ -80,8 +80,8 @@ add_task(async function test_sidebar_in_customize_mode() {
);
// Attempt toggle - should fail in customize mode.
- await SidebarUI.toggle();
- ok(SidebarUI.isOpen, "Sidebar is still open");
+ await SidebarController.toggle();
+ ok(SidebarController.isOpen, "Sidebar is still open");
// Exit customize mode. This should re-enable the toggle and make the sidebar
// toggle widget appear checked again, since toggle() didn't hide the sidebar.
@@ -98,8 +98,8 @@ add_task(async function test_sidebar_in_customize_mode() {
"Sidebar widget background should appear checked again"
);
- await SidebarUI.toggle();
- ok(!SidebarUI.isOpen, "Sidebar is closed");
+ await SidebarController.toggle();
+ ok(!SidebarController.isOpen, "Sidebar is closed");
Assert.equal(
getBGAlpha(),
0,
diff --git a/browser/base/content/test/sidebar/browser_sidebar_move.js b/browser/base/content/test/sidebar/browser_sidebar_move.js
index 3de26b7966..05ea9e3322 100644
--- a/browser/base/content/test/sidebar/browser_sidebar_move.js
+++ b/browser/base/content/test/sidebar/browser_sidebar_move.js
@@ -1,18 +1,20 @@
registerCleanupFunction(() => {
Services.prefs.clearUserPref("sidebar.position_start");
- SidebarUI.hide();
+ SidebarController.hide();
});
const EXPECTED_START_ORDINALS = [
- ["sidebar-box", 1],
- ["sidebar-splitter", 2],
- ["appcontent", 3],
+ ["sidebar-main", 1],
+ ["sidebar-box", 2],
+ ["sidebar-splitter", 3],
+ ["appcontent", 4],
];
const EXPECTED_END_ORDINALS = [
- ["sidebar-box", 3],
- ["sidebar-splitter", 2],
- ["appcontent", 1],
+ ["sidebar-main", 5],
+ ["sidebar-box", 4],
+ ["sidebar-splitter", 3],
+ ["appcontent", 2],
];
function getBrowserChildrenWithOrdinals() {
@@ -23,8 +25,8 @@ function getBrowserChildrenWithOrdinals() {
}
add_task(async function () {
- await SidebarUI.show("viewBookmarksSidebar");
- SidebarUI.showSwitcherPanel();
+ await SidebarController.show("viewBookmarksSidebar");
+ SidebarController.showSwitcherPanel();
let reversePositionButton = document.getElementById(
"sidebar-reverse-position"
@@ -41,8 +43,8 @@ add_task(async function () {
ok(!box.hasAttribute("positionend"), "Positioned start");
// Moved to right
- SidebarUI.reversePosition();
- SidebarUI.showSwitcherPanel();
+ SidebarController.reversePosition();
+ SidebarController.showSwitcherPanel();
Assert.deepEqual(
getBrowserChildrenWithOrdinals(),
EXPECTED_END_ORDINALS,
@@ -56,8 +58,8 @@ add_task(async function () {
ok(box.hasAttribute("positionend"), "Positioned end");
// Moved to back to left
- SidebarUI.reversePosition();
- SidebarUI.showSwitcherPanel();
+ SidebarController.reversePosition();
+ SidebarController.showSwitcherPanel();
Assert.deepEqual(
getBrowserChildrenWithOrdinals(),
EXPECTED_START_ORDINALS,
diff --git a/browser/base/content/test/sidebar/browser_sidebar_persist.js b/browser/base/content/test/sidebar/browser_sidebar_persist.js
index fe67bed9e0..4977c7ef0f 100644
--- a/browser/base/content/test/sidebar/browser_sidebar_persist.js
+++ b/browser/base/content/test/sidebar/browser_sidebar_persist.js
@@ -18,7 +18,7 @@ add_task(async function persist_sidebar_width() {
{
info("Showing new window and setting sidebar box");
const win = await BrowserTestUtils.openNewBrowserWindow();
- await win.SidebarUI.show("viewBookmarksSidebar");
+ await win.SidebarController.show("viewBookmarksSidebar");
win.document.getElementById("sidebar-box").style.width = "100px";
await BrowserTestUtils.closeWindow(win);
}
@@ -26,7 +26,7 @@ add_task(async function persist_sidebar_width() {
{
info("Showing new window and seeing persisted width");
const win = await BrowserTestUtils.openNewBrowserWindow();
- await win.SidebarUI.show("viewBookmarksSidebar");
+ await win.SidebarController.show("viewBookmarksSidebar");
is(
win.document.getElementById("sidebar-box").style.width,
"100px",
diff --git a/browser/base/content/test/sidebar/browser_sidebar_switcher.js b/browser/base/content/test/sidebar/browser_sidebar_switcher.js
index 032c23b029..0fc9e18e01 100644
--- a/browser/base/content/test/sidebar/browser_sidebar_switcher.js
+++ b/browser/base/content/test/sidebar/browser_sidebar_switcher.js
@@ -1,5 +1,5 @@
registerCleanupFunction(() => {
- SidebarUI.hide();
+ SidebarController.hide();
});
/**
@@ -9,14 +9,14 @@ registerCleanupFunction(() => {
*/
function showSwitcherPanelPromise() {
return new Promise(resolve => {
- SidebarUI._switcherPanel.addEventListener(
+ SidebarController._switcherPanel.addEventListener(
"popupshown",
() => {
resolve();
},
{ once: true }
);
- SidebarUI.showSwitcherPanel();
+ SidebarController.showSwitcherPanel();
});
}
@@ -25,7 +25,10 @@ function showSwitcherPanelPromise() {
* @returns Promise which resolves when the popup menu is opened
*/
async function waitForSwitcherPopupShown() {
- return BrowserTestUtils.waitForEvent(SidebarUI._switcherPanel, "popupshown");
+ return BrowserTestUtils.waitForEvent(
+ SidebarController._switcherPanel,
+ "popupshown"
+ );
}
/**
@@ -63,7 +66,7 @@ async function testSidebarMenuKeyToggle(key, sidebarTitle) {
info(`Testing "${key}" key handling of sidebar menu popup items
to access ${sidebarTitle} sidebar`);
- Assert.ok(SidebarUI.isOpen, "Sidebar is open");
+ Assert.ok(SidebarController.isOpen, "Sidebar is open");
let sidebarSwitcher = document.querySelector("#sidebar-switcher-target");
let sidebar = document.getElementById("sidebar");
@@ -89,7 +92,7 @@ async function testSidebarMenuKeyToggle(key, sidebarTitle) {
"The sidebar switcher target button is focused"
);
Assert.equal(
- SidebarUI._switcherPanel.state,
+ SidebarController._switcherPanel.state,
"closed",
"Sidebar menu popup is closed"
);
@@ -102,7 +105,7 @@ async function testSidebarMenuKeyToggle(key, sidebarTitle) {
await promisePopupShown;
Assert.equal(
- SidebarUI._switcherPanel.state,
+ SidebarController._switcherPanel.state,
"open",
"Sidebar menu popup is open"
);
@@ -111,7 +114,7 @@ async function testSidebarMenuKeyToggle(key, sidebarTitle) {
let arrowDown = async (menuitemId, msg) => {
let menuItemActive = BrowserTestUtils.waitForEvent(
- SidebarUI._switcherPanel,
+ SidebarController._switcherPanel,
"DOMMenuItemActive"
);
EventUtils.synthesizeKey("KEY_ArrowDown", {});
@@ -149,18 +152,18 @@ async function testSidebarMenuKeyToggle(key, sidebarTitle) {
info("Testing keyboard navigation when a sidebar menu popup is closed");
Assert.equal(
- SidebarUI._switcherPanel.state,
+ SidebarController._switcherPanel.state,
"closed",
"Sidebar menu popup is closed"
);
// Test the sidebar panel is updated
Assert.equal(
- SidebarUI._box.getAttribute("sidebarcommand"),
+ SidebarController._box.getAttribute("sidebarcommand"),
`view${sidebarTitle}Sidebar` /* e.g. "viewHistorySidebar" */,
`${sidebarTitle} sidebar loaded`
);
Assert.equal(
- SidebarUI.currentID,
+ SidebarController.currentID,
`view${sidebarTitle}Sidebar` /* e.g. "viewHistorySidebar" */,
`${sidebarTitle}'s current ID is updated to a target view`
);
@@ -173,7 +176,7 @@ add_task(async function markup() {
false,
"Unexpected sidebar found - a previous test failed to cleanup correctly"
);
- SidebarUI.hide();
+ SidebarController.hide();
}
let sidebarPopup = document.querySelector("#sidebarMenu-popup");
@@ -205,7 +208,7 @@ add_task(async function markup() {
info("Test dynamic changes in the markup of the sidebar switcher control");
- await SidebarUI.show("viewBookmarksSidebar");
+ await SidebarController.show("viewBookmarksSidebar");
await showSwitcherPanelPromise();
Assert.equal(
@@ -229,25 +232,25 @@ add_task(async function markup() {
"Sidebar switcher button is collapsed when a sidebar menu is dismissed"
);
- SidebarUI.hide();
+ SidebarController.hide();
});
add_task(async function keynav() {
// If a sidebar is already open, close it.
- if (SidebarUI.isOpen) {
+ if (SidebarController.isOpen) {
Assert.ok(
false,
"Unexpected sidebar found - a previous test failed to cleanup correctly"
);
- SidebarUI.hide();
+ SidebarController.hide();
}
- await SidebarUI.show("viewBookmarksSidebar");
+ await SidebarController.show("viewBookmarksSidebar");
await testSidebarMenuKeyToggle("KEY_Enter", "History");
await testSidebarMenuKeyToggle(" ", "Tabs");
- SidebarUI.hide();
+ SidebarController.hide();
});
add_task(async function mouse() {
@@ -257,11 +260,11 @@ add_task(async function mouse() {
false,
"Unexpected sidebar found - a previous test failed to cleanup correctly"
);
- SidebarUI.hide();
+ SidebarController.hide();
}
let sidebar = document.querySelector("#sidebar-box");
- await SidebarUI.show("viewBookmarksSidebar");
+ await SidebarController.show("viewBookmarksSidebar");
await showSwitcherPanelPromise();
await pickSwitcherMenuitem("#sidebar-switcher-history");
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..9668288181 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.
@@ -280,9 +276,6 @@ var allowlist = [
// find the references)
{ file: "chrome://browser/content/screenshots/copied-notification.svg" },
- // Bug 1875361
- { file: "chrome://global/content/ml/SummarizerModel.sys.mjs" },
-
// toolkit/xre/MacRunFromDmgUtils.mm
{ file: "resource://gre/localization/en-US/toolkit/global/run-from-dmg.ftl" },
@@ -291,8 +284,31 @@ var allowlist = [
{ file: "chrome://browser/content/screenshots/copy.svg" },
{ file: "chrome://browser/content/screenshots/download.svg" },
{ file: "chrome://browser/content/screenshots/download-white.svg" },
+
+ // Referenced programmatically
+ { file: "chrome://browser/content/backup/BackupManifest.1.schema.json" },
+
+ // Bug 1892002
+ { file: "resource://app/modules/TopSites.sys.mjs" },
];
+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" },
+
+ // The Transformers.js prod lib is not used in Nightly builds
+ { file: "chrome://global/content/ml/transformers.js" },
+ { file: "chrome://global/content/ml/ort.js" },
+ ]
+ );
+}
+
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..6b075fc98f 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,
},
@@ -42,6 +36,12 @@ let ignoreList = [
errorMessage: /Unknown property.*overflow-clip-box/i,
isFromDevTools: false,
},
+ // content: -moz-alt-content is UA-only.
+ {
+ sourceName: /\b(html)\.css$/i,
+ errorMessage: /Error in parsing value for ‘content’/i,
+ isFromDevTools: false,
+ },
// These variables are declared somewhere else, and error when we load the
// files directly. They're all marked intermittent because their appearance
// in the error console seems to not be consistent.
@@ -123,6 +123,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 +436,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..71a2930ddc 100644
--- a/browser/base/content/test/static/browser_parsable_script.js
+++ b/browser/base/content/test/static/browser_parsable_script.js
@@ -19,12 +19,13 @@ const kESModuleList = new Set([
/browser\/vpn-card.js$/,
/toolkit\/content\/global\/certviewer\/components\/.*\.js$/,
/toolkit\/content\/global\/certviewer\/.*\.js$/,
+ /toolkit\/content\/global\/ml\/transformers.*\.js$/,
/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 +70,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/browser_sentence_case_strings.js b/browser/base/content/test/static/browser_sentence_case_strings.js
index e995f76b1a..12952c9600 100644
--- a/browser/base/content/test/static/browser_sentence_case_strings.js
+++ b/browser/base/content/test/static/browser_sentence_case_strings.js
@@ -103,7 +103,7 @@ function checkSubheaders(view) {
}
async function checkUpdateBanner(view) {
- let banner = view.querySelector("#appMenu-proton-update-banner");
+ let banner = view.querySelector("#appMenu-update-banner");
const notifications = [
"update-downloading",
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_sendpage.js b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
index a80cf8a1d0..14b5f72860 100644
--- a/browser/base/content/test/sync/browser_contextmenu_sendpage.js
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -93,47 +93,44 @@ add_task(async function test_link_contextmenu() {
"context-sendlinktodevice-popup"
);
- let expectedArray = ["context-openlinkintab"];
-
- if (
+ const expectOpenLinkInUserContextMenu =
Services.prefs.getBoolPref("privacy.userContext.enabled") &&
- ContextualIdentityService.getPublicIdentities().length
- ) {
- expectedArray.push("context-openlinkinusercontext-menu");
- }
+ ContextualIdentityService.getPublicIdentities().length;
+
+ const expectStripOnShareLink = Services.prefs.getBoolPref(
+ "privacy.query_stripping.strip_on_share.enabled"
+ );
+
+ const expectTranslateSelection =
+ Services.prefs.getBoolPref("browser.translations.enable") &&
+ Services.prefs.getBoolPref("browser.translations.select.enable");
- expectedArray.push(
+ const expectInspectAccessibility =
+ Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
+ (Services.prefs.getBoolPref("devtools.everOpened", false) ||
+ Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0);
+
+ const expectedArray = [
+ "context-openlinkintab",
+ ...(expectOpenLinkInUserContextMenu
+ ? ["context-openlinkinusercontext-menu"]
+ : []),
"context-openlink",
"context-openlinkprivate",
"context-sep-open",
"context-bookmarklink",
"context-savelink",
"context-savelinktopocket",
- "context-copylink"
- );
-
- if (
- Services.prefs.getBoolPref("privacy.query_stripping.strip_on_share.enabled")
- ) {
- expectedArray.push("context-stripOnShareLink");
- }
-
- expectedArray.push(
+ "context-copylink",
+ ...(expectStripOnShareLink ? ["context-stripOnShareLink"] : []),
"context-sendlinktodevice",
"context-sep-sendlinktodevice",
"context-searchselect",
- "frame-sep"
- );
-
- if (
- Services.prefs.getBoolPref("devtools.accessibility.enabled", true) &&
- (Services.prefs.getBoolPref("devtools.everOpened", false) ||
- Services.prefs.getIntPref("devtools.selfxss.count", 0) > 0)
- ) {
- expectedArray.push("context-inspect-a11y");
- }
-
- expectedArray.push("context-inspect");
+ ...(expectTranslateSelection ? ["context-translate-selection"] : []),
+ "frame-sep",
+ ...(expectInspectAccessibility ? ["context-inspect-a11y"] : []),
+ "context-inspect",
+ ];
let menu = document.getElementById("contentAreaContextMenu");
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..03077ea67c 100644
--- a/browser/base/content/test/sync/browser_sync.js
+++ b/browser/base/content/test/sync/browser_sync.js
@@ -607,17 +607,20 @@ add_task(async function test_experiment_ui_state_unconfigured() {
checkFxAAvatar("not_configured");
let expectedLabel = gSync.fluentStrings.formatValueSync(
- "appmenuitem-sign-in-account"
+ "synced-tabs-fxa-sign-in"
+ );
+
+ let expectedDescriptionLabel = gSync.fluentStrings.formatValueSync(
+ "fxa-menu-sync-description"
);
await openMainPanel();
checkFxaToolbarButtonPanel({
headerTitle: expectedLabel,
- headerDescription: "",
+ headerDescription: expectedDescriptionLabel,
enabledItems: [
"PanelUI-fxa-cta-menu",
- "PanelUI-fxa-menu-sync-button",
"PanelUI-fxa-menu-monitor-button",
"PanelUI-fxa-menu-relay-button",
"PanelUI-fxa-menu-vpn-button",
@@ -690,7 +693,6 @@ add_task(async function test_experiment_ui_state_signedin() {
"PanelUI-fxa-menu-sync-prefs-button",
"PanelUI-fxa-menu-account-signout-button",
"PanelUI-fxa-cta-menu",
- "PanelUI-fxa-menu-sync-button",
"PanelUI-fxa-menu-monitor-button",
"PanelUI-fxa-menu-relay-button",
"PanelUI-fxa-menu-vpn-button",
@@ -772,7 +774,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 +896,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.toml b/browser/base/content/test/tabPrompts/browser.toml
index 037f1f0d2b..aa7d4c724e 100644
--- a/browser/base/content/test/tabPrompts/browser.toml
+++ b/browser/base/content/test/tabPrompts/browser.toml
@@ -39,6 +39,8 @@ support-files = ["file_beforeunload_stop.html"]
https_first_disabled = true
support-files = ["openPromptOffTimeout.html"]
+["browser_promptDelays.js"]
+
["browser_promptFocus.js"]
["browser_prompt_close_event.js"]
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_promptDelays.js b/browser/base/content/test/tabPrompts/browser_promptDelays.js
new file mode 100644
index 0000000000..ecd01cdb69
--- /dev/null
+++ b/browser/base/content/test/tabPrompts/browser_promptDelays.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PERMISSION_DIALOG =
+ "chrome://mozapps/content/handling/permissionDialog.xhtml";
+
+add_setup(async function () {
+ // Set a new handler as default.
+ const protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo("web+testprotocol");
+ protoInfo.preferredAction = protoInfo.useHelperApp;
+ let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ handler.uriTemplate = "https://example.com/foobar?uri=%s";
+ handler.name = "Test protocol";
+ let handlers = protoInfo.possibleApplicationHandlers;
+ handlers.appendElement(handler);
+
+ protoInfo.preferredApplicationHandler = handler;
+ protoInfo.alwaysAskBeforeHandling = false;
+
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ handlerSvc.store(protoInfo);
+
+ registerCleanupFunction(() => {
+ handlerSvc.remove(protoInfo);
+ });
+});
+
+add_task(async function test_promptWhileNotForeground() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let windowOpened = BrowserTestUtils.waitForNewWindow();
+ await SpecialPowers.spawn(browser, [], () => {
+ content.eval(`window.open('about:blank', "_blank", "height=600");`);
+ });
+ let otherWin = await windowOpened;
+ info("Opened extra window, now start a prompt.");
+
+ // To ensure we test the delay helper correctly, shorten the delay:
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.dialog_enable_delay", 50]],
+ });
+
+ let promptPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ PERMISSION_DIALOG,
+ { isSubDialog: true }
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.location.href = "web+testprotocol:hello";
+ });
+ info("Started opening prompt.");
+ let prompt = await promptPromise;
+ info("Opened prompt.");
+ let dialog = prompt.document.querySelector("dialog");
+ let button = dialog.getButton("accept");
+ is(button.getAttribute("disabled"), "true", "Button should be disabled");
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+ is(
+ button.getAttribute("disabled"),
+ "true",
+ "Button should still be disabled while the dialog is in the background"
+ );
+
+ let buttonGetsEnabled = BrowserTestUtils.waitForMutationCondition(
+ button,
+ { attributeFilter: ["disabled"] },
+ () => button.getAttribute("disabled") != "true"
+ );
+ await BrowserTestUtils.closeWindow(otherWin);
+ info("Waiting for button to be enabled.");
+ await buttonGetsEnabled;
+ ok(true, "The button was enabled.");
+ dialog.cancelDialog();
+
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function test_promptWhileForeground() {
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ let promptPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ PERMISSION_DIALOG,
+ { isSubDialog: true }
+ );
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.location.href = "web+testprotocol:hello";
+ });
+ info("Started opening prompt.");
+ let prompt = await promptPromise;
+ info("Opened prompt.");
+ let dialog = prompt.document.querySelector("dialog");
+ let button = dialog.getButton("accept");
+ is(button.getAttribute("disabled"), "true", "Button should be disabled");
+ await BrowserTestUtils.waitForMutationCondition(
+ button,
+ { attributeFilter: ["disabled"] },
+ () => button.getAttribute("disabled") != "true"
+ );
+ ok(true, "The button was enabled.");
+ dialog.cancelDialog();
+ });
+});
diff --git a/browser/base/content/test/tabPrompts/browser_promptFocus.js b/browser/base/content/test/tabPrompts/browser_promptFocus.js
index 89ca064c10..cab812f57b 100644
--- a/browser/base/content/test/tabPrompts/browser_promptFocus.js
+++ b/browser/base/content/test/tabPrompts/browser_promptFocus.js
@@ -20,8 +20,7 @@ add_task(async function test_tabdialogbox_tab_switch_focus() {
tabPromises.push(
BrowserTestUtils.openNewForegroundTab(
gBrowser,
- // eslint-disable-next-line @microsoft/sdl/no-insecure-url
- "http://example.com",
+ "https://example.com",
true
)
);
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..8a95c87a6e 100644
--- a/browser/base/content/test/tabs/browser.toml
+++ b/browser/base/content/test/tabs/browser.toml
@@ -30,6 +30,8 @@ skip-if = [
["browser_bfcache_exemption_about_pages.js"]
skip-if = ["!fission"]
+["browser_blank_tab_label.js"]
+
["browser_bug580956.js"]
["browser_bug_1387976_restore_lazy_tab_browser_muted_state.js"]
@@ -76,6 +78,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"]
@@ -138,6 +142,8 @@ support-files = [
["browser_multiselect_tabs_close.js"]
+["browser_multiselect_tabs_close_duplicate_tabs.js"]
+
["browser_multiselect_tabs_close_other_tabs.js"]
["browser_multiselect_tabs_close_tabs_to_the_left.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_blank_tab_label.js b/browser/base/content/test/tabs/browser_blank_tab_label.js
new file mode 100644
index 0000000000..9fe5f6b1b0
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_blank_tab_label.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Ensure that we don't use an entirely-blank (non-printable) document title
+ * as the tab label.
+ */
+add_task(async function test_ensure_printable_label() {
+ const TEST_DOC = `
+ <!DOCTYPE html>
+ <meta charset="utf-8">
+ <!-- Title is NO-BREAK SPACE, COMBINING ACUTE ACCENT, ARABIC LETTER MARK -->
+ <title>&nbsp;&%23x0301;&%23x061C;</title>
+ Is my title blank?`;
+
+ let newTab;
+ function tabLabelChecker() {
+ Assert.stringMatches(
+ newTab.label,
+ /\p{L}|\p{N}|\p{P}|\p{S}/u,
+ "Tab label should contain printable character."
+ );
+ }
+ let mutationObserver = new MutationObserver(tabLabelChecker);
+ registerCleanupFunction(() => mutationObserver.disconnect());
+
+ gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ event => {
+ newTab = event.target;
+ tabLabelChecker();
+ mutationObserver.observe(newTab, {
+ attributeFilter: ["label"],
+ });
+ },
+ { once: true }
+ );
+
+ await BrowserTestUtils.withNewTab("data:text/html," + TEST_DOC, async () => {
+ // Wait another longer-than-tick to ensure more mutation observer things have
+ // come in.
+ await new Promise(executeSoon);
+
+ // Check one last time for good measure, for the final label:
+ tabLabelChecker();
+ });
+});
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_close_duplicate_tabs.js b/browser/base/content/test/tabs/browser_multiselect_tabs_close_duplicate_tabs.js
new file mode 100644
index 0000000000..d18795447f
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_duplicate_tabs.js
@@ -0,0 +1,178 @@
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+const PREF_SHOWN_DUPE_DIALOG =
+ "browser.tabs.haveShownCloseAllDuplicateTabsWarning";
+
+add_task(async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_WARN_ON_CLOSE, false],
+ [PREF_SHOWN_DUPE_DIALOG, true],
+ ],
+ });
+});
+
+add_task(async function withAMultiSelectedTab() {
+ let initialTab = gBrowser.selectedTab;
+ let tab1 = await addTab();
+ let tab2 = await addTab();
+ let tab3 = await addTab();
+ let tab4 = await addTab();
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await triggerClickOn(tab1, { ctrlKey: true });
+
+ let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned");
+ gBrowser.pinTab(tab4);
+ await tab4Pinned;
+
+ ok(initialTab.multiselected, "InitialTab is multiselected");
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(!tab2.multiselected, "Tab2 is not multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(tab4.pinned, "Tab4 is pinned");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+ is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab");
+
+ let closingTabs = [tab2, tab3];
+ let tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ gBrowser.removeDuplicateTabs(tab1);
+
+ await Promise.all(tabClosingPromises);
+
+ ok(!initialTab.closing, "InitialTab is not closing");
+ ok(!tab1.closing, "Tab1 is not closing");
+ ok(tab2.closing, "Tab2 is closing");
+ ok(tab3.closing, "Tab3 is closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+ is(gBrowser.selectedTab, initialTab, "InitialTab is still the active tab");
+
+ gBrowser.clearMultiSelectedTabs();
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab4);
+});
+
+add_task(async function withNotAMultiSelectedTab() {
+ let initialTab = gBrowser.selectedTab;
+ let tab1 = await addTab("http://mochi.test:8888/");
+ let tab2 = await addTab("http://mochi.test:8888/");
+ let tab3 = await addTab("http://mochi.test:8888/");
+ let tab4 = await addTab("http://mochi.test:8888/");
+ let tab5 = await addTab("http://mochi.test:8888/");
+ let tab6 = await addTab("http://mochi.test:8888/", { userContextId: 1 });
+
+ is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+ await triggerClickOn(tab2, { ctrlKey: true });
+ await triggerClickOn(tab5, { ctrlKey: true });
+
+ let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned");
+ gBrowser.pinTab(tab4);
+ await tab4Pinned;
+
+ let tab5Pinned = BrowserTestUtils.waitForEvent(tab5, "TabPinned");
+ gBrowser.pinTab(tab5);
+ await tab5Pinned;
+
+ ok(!initialTab.multiselected, "InitialTab is not multiselected");
+ ok(tab1.multiselected, "Tab1 is multiselected");
+ ok(tab2.multiselected, "Tab2 is multiselected");
+ ok(!tab3.multiselected, "Tab3 is not multiselected");
+ ok(!tab4.multiselected, "Tab4 is not multiselected");
+ ok(tab4.pinned, "Tab4 is pinned");
+ ok(tab5.multiselected, "Tab5 is multiselected");
+ ok(tab5.pinned, "Tab5 is pinned");
+ ok(!tab6.multiselected, "Tab6 is not multiselected");
+ ok(!tab6.pinned, "Tab6 is not pinned");
+ is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+ is(gBrowser.selectedTab, tab1, "Tab1 is the active tab");
+
+ let closingTabs = [tab1, tab2];
+ let tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ gBrowser.removeDuplicateTabs(tab3)
+ );
+
+ await Promise.all(tabClosingPromises);
+
+ ok(!initialTab.closing, "InitialTab is not closing");
+ ok(tab1.closing, "Tab1 is closing");
+ ok(tab2.closing, "Tab2 is closing");
+ ok(!tab3.closing, "Tab3 is not closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ ok(!tab5.closing, "Tab5 is not closing");
+ ok(!tab6.closing, "Tab6 is not closing");
+ is(
+ gBrowser.multiSelectedTabsCount,
+ 0,
+ "Zero multiselected tabs, selection is cleared"
+ );
+ is(gBrowser.selectedTab, tab3, "tab3 is the active tab now");
+
+ for (let tab of [tab3, tab4, tab5, tab6]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function closeAllDuplicateTabs() {
+ let initialTab = gBrowser.selectedTab;
+ let tab1 = await addTab("http://mochi.test:8888/one");
+ let tab2 = await addTab("http://mochi.test:8888/two", { userContextId: 1 });
+ let tab3 = await addTab("http://mochi.test:8888/one");
+ let tab4 = await addTab("http://mochi.test:8888/two");
+ let tab5 = await addTab("http://mochi.test:8888/one");
+ let tab6 = await addTab("http://mochi.test:8888/two");
+
+ let tab1Pinned = BrowserTestUtils.waitForEvent(tab1, "TabPinned");
+ gBrowser.pinTab(tab1);
+ await tab1Pinned;
+
+ // So we have 1p,2c,1,2,1,2
+ // We expect 1p,2c,X,2,X,X because the pinned 1 will dupe the other two 1,
+ // but the 2c's userContextId makes it unique against the other two 2,
+ // but one of the other two 2 will close.
+
+ // Ensure tab4 remains by making it active more recently than tab6.
+ tab4._lastSeenActive = Date.now(); // as recent as it gets.
+
+ // Assert some preconditions:
+ ok(tab1.pinned, "Tab1 is pinned");
+ Assert.greater(tab4.lastSeenActive, tab6.lastSeenActive);
+
+ let closingTabs = [tab3, tab5, tab6];
+ let tabClosingPromises = [];
+ for (let tab of closingTabs) {
+ tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+ }
+
+ await BrowserTestUtils.switchTab(
+ gBrowser,
+ gBrowser.removeAllDuplicateTabs(initialTab)
+ );
+
+ await Promise.all(tabClosingPromises);
+
+ ok(!initialTab.closing, "InitialTab is not closing");
+ ok(!tab1.closing, "Tab1 is not closing");
+ ok(!tab2.closing, "Tab2 is not closing");
+ ok(tab3.closing, "Tab3 is closing");
+ ok(!tab4.closing, "Tab4 is not closing");
+ ok(tab5.closing, "Tab5 is closing");
+ ok(tab6.closing, "Tab6 is closing");
+
+ for (let tab of [tab1, tab2, tab4]) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
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..19ba85b9f8 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",
@@ -34,6 +34,7 @@ add_setup(async function () {
set: [
["browser.tabs.cardPreview.enabled", true],
["browser.tabs.cardPreview.showThumbnails", false],
+ ["browser.tabs.tooltipsShowPidAndActiveness", false],
["ui.tooltip.delay_ms", 0],
],
});
@@ -53,38 +54,120 @@ 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);
+
+ // Move the mouse outside of the tab strip.
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mouseover",
+ });
+});
+
+/**
+ * Verify that the pid and activeness statuses are not shown
+ * when the flag is not enabled.
+ */
+add_task(async function pidAndActivenessHiddenByDefaultTests() {
+ 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 previewContainer = document.getElementById("tab-preview-panel");
+
+ await openPreview(tab1);
+ Assert.equal(
+ previewContainer.querySelector(".tab-preview-pid").innerText,
+ "",
+ "Tab PID is not shown"
+ );
+ Assert.equal(
+ previewContainer.querySelector(".tab-preview-activeness").innerText,
+ "",
+ "Tab activeness is not shown"
);
+ await closePreviews();
+
+ BrowserTestUtils.removeTab(tab1);
+
+ // Move the mouse outside of the tab strip.
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {
+ type: "mouseover",
+ });
+});
+
+add_task(async function pidAndActivenessTests() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.tooltipsShowPidAndActiveness", true]],
+ });
+
+ const tabUrl1 =
+ "data:text/html,<html><head><title>Single process tab</title></head><body>Hello</body></html>";
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl1);
+ const tabUrl2 = `data:text/html,<html>
+ <head>
+ <title>Multi-process tab</title>
+ </head>
+ <body>
+ <iframe
+ id="inlineFrameExample"
+ title="Inline Frame Example"
+ width="300"
+ height="200"
+ src="https://example.com">
+ </iframe>
+ </body>
+ </html>`;
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl2);
+ const previewContainer = document.getElementById("tab-preview-panel");
+
+ await openPreview(tab1);
+ Assert.stringMatches(
+ previewContainer.querySelector(".tab-preview-pid").innerText,
+ /^pid: \d+$/,
+ "Tab PID is shown on single process tab"
+ );
+ Assert.equal(
+ previewContainer.querySelector(".tab-preview-activeness").innerText,
+ "",
+ "Tab activeness is not shown on inactive tab"
+ );
+ await closePreviews();
+
+ await openPreview(tab2);
+ Assert.stringMatches(
+ previewContainer.querySelector(".tab-preview-pid").innerText,
+ /^pids: \d+, \d+$/,
+ "Tab PIDs are shown on multi-process tab"
+ );
+ Assert.equal(
+ previewContainer.querySelector(".tab-preview-activeness").innerText,
+ "[A]",
+ "Tab activeness is shown on active tab"
+ );
+ await closePreviews();
+
BrowserTestUtils.removeTab(tab1);
BrowserTestUtils.removeTab(tab2);
+ await SpecialPowers.popPrefEnv();
// Move the mouse outside of the tab strip.
EventUtils.synthesizeMouseAtCenter(document.documentElement, {
@@ -105,29 +188,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 +239,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 +346,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_visibleTabs_contextMenu.js b/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js
index 202c43ce47..06fdd27d9c 100644
--- a/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js
+++ b/browser/base/content/test/tabs/browser_visibleTabs_contextMenu.js
@@ -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 remoteClientsFixture = [
- { id: 1, name: "Foo" },
- { id: 2, name: "Bar" },
-];
-
add_task(async function test() {
// There should be one tab when we start the test
let [origTab] = gBrowser.visibleTabs;
@@ -16,9 +11,8 @@ add_task(async function test() {
// Check the context menu with two tabs
updateTabContextMenu(origTab);
- is(
- document.getElementById("context_closeTab").disabled,
- false,
+ ok(
+ !document.getElementById("context_closeTab").disabled,
"Close Tab is enabled"
);
@@ -29,11 +23,14 @@ add_task(async function test() {
// Check the context menu with one tab.
updateTabContextMenu(testTab);
- is(
- document.getElementById("context_closeTab").disabled,
- false,
+ ok(
+ !document.getElementById("context_closeTab").disabled,
"Close Tab is enabled when more than one tab exists"
);
+ ok(
+ !document.getElementById("context_closeDuplicateTabs").disabled,
+ "Close duplicate tabs is enabled when more than one tab with the same URL exists"
+ );
// Add a tab that will get pinned
// So now there's one pinned tab, one visible unpinned tab, and one hidden tab
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_aboutaddons_blanktab.js b/browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js
index 228fe71815..1e0814ea96 100644
--- a/browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js
+++ b/browser/base/content/test/webextensions/browser_aboutaddons_blanktab.js
@@ -8,7 +8,7 @@ add_task(async function testBlankTabReusedAboutAddons() {
is(browser, gBrowser.selectedBrowser, "New tab is selected");
// Opening about:addons shouldn't change the selected tab.
- BrowserOpenAddonsMgr();
+ BrowserAddonUI.openAddonsMgr();
is(browser, gBrowser.selectedBrowser, "No new tab was opened");
diff --git a/browser/base/content/test/webextensions/browser_extension_sideloading.js b/browser/base/content/test/webextensions/browser_extension_sideloading.js
index 4e1fe07194..5d8f82a178 100644
--- a/browser/base/content/test/webextensions/browser_extension_sideloading.js
+++ b/browser/base/content/test/webextensions/browser_extension_sideloading.js
@@ -16,11 +16,14 @@ const kSideloaded = true;
async function createWebExtension(details) {
let options = {
manifest: {
+ manifest_version: details.manifest_version ?? 2,
+
browser_specific_settings: { gecko: { id: details.id } },
name: details.name,
permissions: details.permissions,
+ host_permissions: details.host_permissions,
},
};
@@ -86,9 +89,10 @@ add_task(async function test_sideloading() {
const ID2 = "addon2@tests.mozilla.org";
await createWebExtension({
+ manifest_version: 3,
id: ID2,
name: "Test 2",
- permissions: ["<all_urls>"],
+ host_permissions: ["<all_urls>"],
});
const ID3 = "addon3@tests.mozilla.org";
@@ -224,7 +228,7 @@ add_task(async function test_sideloading() {
// Close the hamburger menu and go directly to the addons manager
await gCUITestUtils.hideMainMenu();
- win = await BrowserOpenAddonsMgr(VIEW);
+ win = await BrowserAddonUI.openAddonsMgr(VIEW);
await waitAboutAddonsViewLoaded(win.document);
// about:addons addon entry element.
@@ -293,7 +297,7 @@ add_task(async function test_sideloading() {
// Close the hamburger menu and go to the detail page for this addon
await gCUITestUtils.hideMainMenu();
- win = await BrowserOpenAddonsMgr(
+ win = await BrowserAddonUI.openAddonsMgr(
`addons://detail/${encodeURIComponent(ID3)}`
);
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
index a3bdf6f832..afd0a8bcee 100644
--- a/browser/base/content/test/webextensions/browser_legacy_webext.xpi
+++ b/browser/base/content/test/webextensions/browser_legacy_webext.xpi
Binary files differ
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..731e8adea7 100644
--- a/browser/base/content/test/webextensions/browser_permissions_local_file.js
+++ b/browser/base/content/test/webextensions/browser_permissions_local_file.js
@@ -14,7 +14,9 @@ async function installFile(filename) {
MockFilePicker.setFiles([file]);
MockFilePicker.afterOpenCallback = MockFilePicker.cleanup;
- let { document } = await BrowserOpenAddonsMgr("addons://list/extension");
+ let { document } = await BrowserAddonUI.openAddonsMgr(
+ "addons://list/extension"
+ );
// Do the install...
await waitAboutAddonsViewLoaded(document);
@@ -32,9 +34,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_update_interactive_noprompt.js b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
index 0b0b912503..9ad3deaae1 100644
--- a/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
+++ b/browser/base/content/test/webextensions/browser_update_interactive_noprompt.js
@@ -36,7 +36,7 @@ async function testUpdateNoPrompt(
is(addon.version, initialVersion, "Version 1 of the addon is installed");
// Go to Extensions in about:addons
- let win = await BrowserOpenAddonsMgr("addons://list/extension");
+ let win = await BrowserAddonUI.openAddonsMgr("addons://list/extension");
await waitAboutAddonsViewLoaded(win.document);
diff --git a/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi b/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi
index ab97d96a11..87500ceb38 100644
--- a/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi
+++ b/browser/base/content/test/webextensions/browser_webext_nopermissions.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_permissions.xpi b/browser/base/content/test/webextensions/browser_webext_permissions.xpi
index a8c8c38ef8..8149ce7b6b 100644
--- a/browser/base/content/test/webextensions/browser_webext_permissions.xpi
+++ b/browser/base/content/test/webextensions/browser_webext_permissions.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update1.xpi b/browser/base/content/test/webextensions/browser_webext_update1.xpi
index 086b3839b9..66ad3e1b31 100644
--- a/browser/base/content/test/webextensions/browser_webext_update1.xpi
+++ b/browser/base/content/test/webextensions/browser_webext_update1.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update2.xpi b/browser/base/content/test/webextensions/browser_webext_update2.xpi
index 19967c39c0..a120a64c6d 100644
--- a/browser/base/content/test/webextensions/browser_webext_update2.xpi
+++ b/browser/base/content/test/webextensions/browser_webext_update2.xpi
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi b/browser/base/content/test/webextensions/browser_webext_update_icon1.xpi
index 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
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi b/browser/base/content/test/webextensions/browser_webext_update_icon2.xpi
index 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
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi b/browser/base/content/test/webextensions/browser_webext_update_perms1.xpi
index 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
Binary files differ
diff --git a/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi b/browser/base/content/test/webextensions/browser_webext_update_perms2.xpi
index 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
Binary files differ
diff --git a/browser/base/content/test/webextensions/head.js b/browser/base/content/test/webextensions/head.js
index 84f7cd02d7..f364dbed88 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";
@@ -508,7 +508,7 @@ async function interactiveUpdateTest(autoUpdate, checkFn) {
ok(addon, "Addon was installed");
is(addon.version, "1.0", "Version 1 of the addon is installed");
- let win = await BrowserOpenAddonsMgr("addons://list/extension");
+ let win = await BrowserAddonUI.openAddonsMgr("addons://list/extension");
await waitAboutAddonsViewLoaded(win.document);
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..1d8637db2e 100644
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -113,11 +113,6 @@ function getRootEvent(aEvent) {
return BrowserUtils.getRootEvent(aEvent);
}
-// This is here for historical reasons. bug 1742889 covers cleaning this up.
-function whereToOpenLink(e, ignoreButton, ignoreAlt) {
- return BrowserUtils.whereToOpenLink(e, ignoreButton, ignoreAlt);
-}
-
function openTrustedLinkIn(url, where, params) {
URILoadingHelper.openTrustedLinkIn(window, url, where, params);
}
@@ -294,7 +289,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 +336,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/webext-panels.xhtml b/browser/base/content/webext-panels.xhtml
index f421d9bf80..1b97794a8d 100644
--- a/browser/base/content/webext-panels.xhtml
+++ b/browser/base/content/webext-panels.xhtml
@@ -28,7 +28,7 @@
<html:link rel="localization" href="toolkit/branding/brandings.ftl"/>
<html:link rel="localization" href="toolkit/global/textActions.ftl"/>
<html:link rel="localization" href="browser/browserContext.ftl"/>
- <html:link rel="localization" href="preview/select-translations.ftl"/>
+ <html:link rel="localization" href="browser/translations.ftl"/>
</linkset>
<commandset id="mainCommandset">
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..45a202b086 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)
@@ -49,12 +50,12 @@ browser.jar:
content/browser/browser-fullScreenAndPointerLock.js (content/browser-fullScreenAndPointerLock.js)
content/browser/browser-fullZoom.js (content/browser-fullZoom.js)
content/browser/browser-gestureSupport.js (content/browser-gestureSupport.js)
+ content/browser/browser-init.js (content/browser-init.js)
content/browser/browser-pageActions.js (content/browser-pageActions.js)
content/browser/browser-pagestyle.js (content/browser-pagestyle.js)
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)
@@ -79,7 +80,6 @@ browser.jar:
content/browser/sanitize_v2.xhtml (content/sanitize_v2.xhtml)
content/browser/sanitizeDialog.js (content/sanitizeDialog.js)
content/browser/sanitizeDialog.css (content/sanitizeDialog.css)
- content/browser/tabbrowser.css (content/tabbrowser.css)
content/browser/tabbrowser.js (content/tabbrowser.js)
content/browser/tabbrowser-tab.js (content/tabbrowser-tab.js)
content/browser/tabbrowser-tabs.js (content/tabbrowser-tabs.js)
diff --git a/browser/base/triage.json b/browser/base/triage.json
index 0f219f87a8..eece04fc32 100644
--- a/browser/base/triage.json
+++ b/browser/base/triage.json
@@ -1,6 +1,6 @@
{
"triagers": {
- "Gijs": {
+ "Gijs Kruitbosch": {
"bzmail": "gijskruitbosch+bugs@gmail.com"
},
"Mark Banner": {
@@ -17,13 +17,54 @@
},
"Fred Chasen": {
"bzmail": "fchasen@mozilla.com"
+ },
+ "Dan Mosedale": {
+ "bzmail": "dmose@mozilla.org"
+ },
+ "Hanna Jones": {
+ "bzmail": "hjones@mozilla.com"
+ },
+ "Nick Alexander": {
+ "bzmail": "nalexander@mozilla.com"
+ },
+ "Mandy Cheang": {
+ "bzmail": "mcheang@mozilla.com"
+ },
+ "Katherine Patenio": {
+ "bzmail": "kpatenio@mozilla.com"
+ },
+ "Robin Steuber": {
+ "bzmail": "bytesized@mozilla.com"
+ },
+ "Mark Striemer": {
+ "bzmail": "mstriemer@mozilla.com"
+ },
+ "Sarah Clements": {
+ "bzmail": "sclements@mozilla.com"
+ },
+ "Jared Hirsch": {
+ "bzmail": "jhirsch@mozilla.com"
+ },
+ "Kelly Cochrane": {
+ "bzmail": "kcochrane@mozilla.com"
}
},
"duty-start-dates": {
- "2024-01-01": "Gijs",
- "2024-02-01": "Mark Banner",
- "2024-03-01": "Fred Chasen",
"2024-04-01": "Mike Conley",
- "2024-05-01": "Marco Bonardo"
+ "2024-05-01": "Marco Bonardo",
+ "2024-06-01": "Dan Mosedale",
+ "2024-07-01": "Gijs Kruitbosch",
+ "2024-08-01": "Hanna Jones",
+ "2024-09-01": "Sarah Clements",
+ "2024-10-01": "Nick Alexander",
+ "2024-11-01": "Mandy Cheang",
+ "2024-12-01": "Katherine Patenio",
+ "2025-01-01": "Mike Conley",
+ "2025-02-01": "Robin Steuber",
+ "2025-03-01": "Dave Townsend",
+ "2025-04-01": "Mark Striemer",
+ "2025-05-01": "Marco Bonardo",
+ "2025-06-01": "Jared Hirsch",
+ "2025-07-01": "Kelly Cochrane"
}
}
diff --git a/browser/branding/aurora/pref/firefox-branding.js b/browser/branding/aurora/pref/firefox-branding.js
index 526997980e..0e257a777d 100644
--- a/browser/branding/aurora/pref/firefox-branding.js
+++ b/browser/branding/aurora/pref/firefox-branding.js
@@ -7,7 +7,7 @@
pref("startup.homepage_welcome_url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%a2/firstrun/");
pref("startup.homepage_welcome_url.additional", "");
// The time interval between checks for a new version (in seconds)
-pref("app.update.interval", 28800); // 8 hours
+pref("app.update.interval", 21600); // 6 hours
// Give the user x seconds to react before showing the big UI. default=192 hours
pref("app.update.promptWaitTime", 691200);
// URL user can browse to manually if for some reason all update installation
diff --git a/browser/branding/official/pref/firefox-branding.js b/browser/branding/official/pref/firefox-branding.js
index 5125604fe9..25711ea79f 100644
--- a/browser/branding/official/pref/firefox-branding.js
+++ b/browser/branding/official/pref/firefox-branding.js
@@ -8,7 +8,7 @@ pref("startup.homepage_override_url", "");
pref("startup.homepage_welcome_url", "about:welcome");
pref("startup.homepage_welcome_url.additional", "");
// Interval: Time between checks for a new version (in seconds)
-pref("app.update.interval", 43200); // 12 hours
+pref("app.update.interval", 21600); // 6 hours
// Give the user x seconds to react before showing the big UI. default=192 hours
pref("app.update.promptWaitTime", 691200);
// app.update.url.manual: URL user can browse to manually if for some reason
diff --git a/browser/components/BrowserContentHandler.sys.mjs b/browser/components/BrowserContentHandler.sys.mjs
index ca7cf4d2c4..6c156d1700 100644
--- a/browser/components/BrowserContentHandler.sys.mjs
+++ b/browser/components/BrowserContentHandler.sys.mjs
@@ -60,62 +60,8 @@ function shouldLoadURI(aURI) {
return false;
}
-function validateFirefoxProtocol(aCmdLine, launchedWithArg_osint) {
- let paramCount = 0;
- // Only accept one parameter when we're handling the protocol.
- for (let i = 0; i < aCmdLine.length; i++) {
- if (!aCmdLine.getArgument(i).startsWith("-")) {
- paramCount++;
- }
- if (paramCount > 1) {
- return false;
- }
- }
- // `-osint` and handling registered file types and protocols is Windows-only.
- return AppConstants.platform != "win" || launchedWithArg_osint;
-}
-
-function resolveURIInternal(
- aCmdLine,
- aArgument,
- launchedWithArg_osint = false
-) {
+function resolveURIInternal(aCmdLine, aArgument) {
let principal = lazy.gSystemPrincipal;
-
- // If using Firefox protocol handler remove it from URI
- // at this stage. This is before we would otherwise
- // record telemetry so do that here.
- let handleFirefoxProtocol = protocol => {
- let protocolWithColon = protocol + ":";
- if (aArgument.startsWith(protocolWithColon)) {
- if (!validateFirefoxProtocol(aCmdLine, launchedWithArg_osint)) {
- throw new Error(
- "Invalid use of Firefox-bridge and Firefox-private-bridge protocols."
- );
- }
- aArgument = aArgument.substring(protocolWithColon.length);
-
- if (
- !aArgument.startsWith("http://") &&
- !aArgument.startsWith("https://")
- ) {
- throw new Error(
- "Firefox-bridge and Firefox-private-bridge protocols can only be used in conjunction with http and https urls."
- );
- }
-
- principal = Services.scriptSecurityManager.createNullPrincipal({});
- Services.telemetry.keyedScalarAdd(
- "os.environment.launched_to_handle",
- protocol,
- 1
- );
- }
- };
-
- handleFirefoxProtocol("firefox-bridge");
- handleFirefoxProtocol("firefox-private-bridge");
-
var uri = aCmdLine.resolveURI(aArgument);
var uriFixup = Services.uriFixup;
@@ -376,12 +322,7 @@ function openBrowserWindow(
Ci.nsILoadContext
).usePrivateBrowsing = true;
- if (
- AppConstants.platform == "win" &&
- lazy.NimbusFeatures.majorRelease2022.getVariable(
- "feltPrivacyWindowSeparation"
- )
- ) {
+ if (AppConstants.platform == "win") {
lazy.WinTaskbar.setGroupIdForWindow(
win,
lazy.WinTaskbar.defaultPrivateGroupId
@@ -438,7 +379,7 @@ function openBrowserWindow(
});
}
-function openPreferences(cmdLine, extraArgs) {
+function openPreferences(cmdLine) {
openBrowserWindow(cmdLine, lazy.gSystemPrincipal, "about:preferences");
}
@@ -602,17 +543,7 @@ nsBrowserContentHandler.prototype = {
"private-window",
false
);
- // Check for Firefox private browsing protocol handler here.
- let url = null;
- let urlFlagIdx = cmdLine.findFlag("url", false);
- if (urlFlagIdx > -1 && cmdLine.length > 1) {
- url = cmdLine.getArgument(urlFlagIdx + 1);
- }
- if (privateWindowParam || url?.startsWith("firefox-private-bridge:")) {
- // Check if the osint flag is present on Windows
- let launchedWithArg_osint =
- AppConstants.platform == "win" &&
- cmdLine.findFlag("osint", false) == 0;
+ if (privateWindowParam) {
let forcePrivate = true;
let resolvedInfo;
if (!lazy.PrivateBrowsingUtils.enabled) {
@@ -623,19 +554,8 @@ nsBrowserContentHandler.prototype = {
uri: Services.io.newURI("about:privatebrowsing"),
principal: lazy.gSystemPrincipal,
};
- } else if (url?.startsWith("firefox-private-bridge:")) {
- cmdLine.removeArguments(urlFlagIdx, urlFlagIdx + 1);
- resolvedInfo = resolveURIInternal(
- cmdLine,
- url,
- launchedWithArg_osint
- );
} else {
- resolvedInfo = resolveURIInternal(
- cmdLine,
- privateWindowParam,
- launchedWithArg_osint
- );
+ resolvedInfo = resolveURIInternal(cmdLine, privateWindowParam);
}
handURIToExistingBrowser(
resolvedInfo.uri,
@@ -1244,24 +1164,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 +1183,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 +1202,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 +1258,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(() => {
@@ -1431,11 +1350,7 @@ nsDefaultCommandLineHandler.prototype = {
try {
var ar;
while ((ar = cmdLine.handleFlagWithParam("url", false))) {
- let { uri, principal } = resolveURIInternal(
- cmdLine,
- ar,
- launchedWithArg_osint
- );
+ let { uri, principal } = resolveURIInternal(cmdLine, ar);
urilist.push(uri);
principalList.push(principal);
@@ -1507,9 +1422,6 @@ nsDefaultCommandLineHandler.prototype = {
}
// Can't open multiple URLs without using system principal.
- // The firefox-bridge and firefox-private-bridge protocols should only
- // accept a single URL due to using the -osint option
- // so this isn't very relevant.
var URLlist = urilist.filter(shouldLoadURI).map(u => u.spec);
if (URLlist.length) {
openBrowserWindow(cmdLine, lazy.gSystemPrincipal, URLlist);
diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs
index f4ea0c87a3..b6ae665df0 100644
--- a/browser/components/BrowserGlue.sys.mjs
+++ b/browser/components/BrowserGlue.sys.mjs
@@ -24,12 +24,14 @@ ChromeUtils.defineESModuleGetters(lazy, {
BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs",
BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs",
BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
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",
@@ -41,11 +43,13 @@ ChromeUtils.defineESModuleGetters(lazy, {
FeatureGate: "resource://featuregates/FeatureGate.sys.mjs",
FirefoxBridgeExtensionUtils:
"resource:///modules/FirefoxBridgeExtensionUtils.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
HomePage: "resource:///modules/HomePage.sys.mjs",
Integration: "resource://gre/modules/Integration.sys.mjs",
Interactions: "resource:///modules/Interactions.sys.mjs",
LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs",
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
@@ -80,8 +84,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",
@@ -124,11 +127,7 @@ XPCOMUtils.defineLazyServiceGetters(lazy, {
ChromeUtils.defineLazyGetter(
lazy,
"accountsL10n",
- () =>
- new Localization(
- ["browser/accounts.ftl", "toolkit/branding/accounts.ftl"],
- true
- )
+ () => new Localization(["browser/accounts.ftl"], true)
);
if (AppConstants.ENABLE_WEBDRIVER) {
@@ -223,6 +222,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",
@@ -410,6 +425,20 @@ let JSWINDOWACTORS = {
enablePreference: "browser.aboutwelcome.enabled",
},
+ BackupUI: {
+ parent: {
+ esModuleURI: "resource:///actors/BackupUIParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource:///actors/BackupUIChild.sys.mjs",
+ events: {
+ "BackupUI:InitWidget": { wantUntrusted: true },
+ },
+ },
+ matches: ["about:preferences*", "about:settings*"],
+ enablePreference: "browser.backup.preferences.ui.enabled",
+ },
+
BlockedSite: {
parent: {
esModuleURI: "resource:///actors/BlockedSiteParent.sys.mjs",
@@ -720,6 +749,7 @@ let JSWINDOWACTORS = {
"Screenshots:OverlaySelection": {},
"Screenshots:RecordEvent": {},
"Screenshots:ShowPanel": {},
+ "Screenshots:FocusPanel": {},
},
},
enablePreference: "screenshots.browser.component.enabled",
@@ -1133,6 +1163,9 @@ BrowserGlue.prototype = {
case "fxaccounts:commands:open-uri":
this._onDisplaySyncURIs(subject);
break;
+ case "fxaccounts:commands:close-uri":
+ this._onIncomingCloseTabCommand(subject);
+ break;
case "session-save":
this._setPrefToSaveSession(true);
subject.QueryInterface(Ci.nsISupportsPRBool);
@@ -1182,13 +1215,13 @@ BrowserGlue.prototype = {
case "initial-migration-did-import-default-bookmarks":
this._initPlaces(true);
break;
- case "handle-xul-text-link":
+ case "handle-xul-text-link": {
let linkHandled = subject.QueryInterface(Ci.nsISupportsPRBool);
if (!linkHandled.data) {
let win = lazy.BrowserWindowTracker.getTopWindow();
if (win) {
data = JSON.parse(data);
- let where = win.whereToOpenLink(data);
+ let where = lazy.BrowserUtils.whereToOpenLink(data);
// Preserve legacy behavior of non-modifier left-clicks
// opening in a new selected tab.
if (where == "current") {
@@ -1199,13 +1232,14 @@ BrowserGlue.prototype = {
}
}
break;
+ }
case "profile-before-change":
// Any component depending on Places should be finalized in
// _onPlacesShutdown. Any component that doesn't need to act after
// the UI has gone should be finalized in _onQuitApplicationGranted.
this._dispose();
break;
- case "keyword-search":
+ case "keyword-search": {
// This notification is broadcast by the docshell when it "fixes up" a
// URI that it's been asked to load into a keyword search.
let engine = null;
@@ -1223,13 +1257,15 @@ BrowserGlue.prototype = {
"urlbar"
);
break;
- case "xpi-signature-changed":
+ }
+ case "xpi-signature-changed": {
let disabledAddons = JSON.parse(data).disabled;
let addons = await lazy.AddonManager.getAddonsByIDs(disabledAddons);
if (addons.some(addon => addon)) {
this._notifyUnsignedAddonsDisabled();
}
break;
+ }
case "sync-ui-state:update":
this._updateFxaBadges(lazy.BrowserWindowTracker.getTopWindow());
break;
@@ -1247,7 +1283,7 @@ BrowserGlue.prototype = {
lazy.DownloadsViewableInternally.register();
break;
- case "app-startup":
+ case "app-startup": {
this._earlyBlankFirstPaint(subject);
gThisInstanceIsTaskbarTab = subject.handleFlag("taskbar-tab", false);
gThisInstanceIsLaunchOnLogin = subject.handleFlag(
@@ -1281,6 +1317,7 @@ BrowserGlue.prototype = {
await lazy.WindowsLaunchOnLogin.removeLaunchOnLoginRegistryKey();
}
break;
+ }
}
},
@@ -1300,6 +1337,7 @@ BrowserGlue.prototype = {
"fxaccounts:verify_login",
"fxaccounts:device_disconnected",
"fxaccounts:commands:open-uri",
+ "fxaccounts:commands:close-uri",
"session-save",
"places-init-complete",
"distribution-customization-complete",
@@ -1430,6 +1468,17 @@ BrowserGlue.prototype = {
lazy.PdfJs.checkIsDefault(this._isNewProfile);
}
+ if (!AppConstants.NIGHTLY_BUILD && this._isNewProfile) {
+ lazy.FormAutofillUtils.setOSAuthEnabled(
+ lazy.FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ false
+ );
+ lazy.LoginHelper.setOSAuthEnabled(
+ lazy.LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF,
+ false
+ );
+ }
+
listeners.init();
lazy.SessionStore.init();
@@ -1614,7 +1663,9 @@ BrowserGlue.prototype = {
"unsignedAddonsDisabled.learnMore.accesskey"
),
callback() {
- win.BrowserOpenAddonsMgr("addons://list/extension?unsigned=true");
+ win.BrowserAddonUI.openAddonsMgr(
+ "addons://list/extension?unsigned=true"
+ );
},
},
];
@@ -2111,7 +2162,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 +2392,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 +2487,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 +2713,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."
+ );
+ }
},
},
@@ -2680,12 +2743,6 @@ BrowserGlue.prototype = {
name: "ensurePrivateBrowsingShortcutExists",
condition:
AppConstants.platform == "win" &&
- // Pref'ed off until Private Browsing window separation is enabled by default
- // to avoid a situation where a user pins the Private Browsing shortcut to
- // the Taskbar, which will end up launching into a different Taskbar icon.
- lazy.NimbusFeatures.majorRelease2022.getVariable(
- "feltPrivacyWindowSeparation"
- ) &&
// We don't want a shortcut if it's been disabled, eg: by enterprise policy.
lazy.PrivateBrowsingUtils.enabled &&
// Private Browsing shortcuts for packaged builds come with the package,
@@ -2907,7 +2964,7 @@ BrowserGlue.prototype = {
let cfg = lazy.NimbusFeatures.gleanInternalSdk.getVariable(
"gleanMetricConfiguration"
);
- Services.fog.setMetricsFeatureConfig(JSON.stringify(cfg));
+ Services.fog.applyServerKnobsConfig(JSON.stringify(cfg));
});
// Register Glean to listen for experiment updates releated to the
@@ -2916,7 +2973,7 @@ BrowserGlue.prototype = {
let cfg = lazy.NimbusFeatures.glean.getVariable(
"gleanMetricConfiguration"
);
- Services.fog.setMetricsFeatureConfig(JSON.stringify(cfg));
+ Services.fog.applyServerKnobsConfig(JSON.stringify(cfg));
});
},
},
@@ -3058,11 +3115,9 @@ BrowserGlue.prototype = {
{
name: "DAPTelemetrySender.startup",
- condition:
- lazy.TelemetryUtils.isTelemetryEnabled &&
- lazy.NimbusFeatures.dapTelemetry.getVariable("enabled"),
- task: () => {
- lazy.DAPTelemetrySender.startup();
+ condition: lazy.TelemetryUtils.isTelemetryEnabled,
+ task: async () => {
+ await lazy.DAPTelemetrySender.startup();
},
},
@@ -3082,9 +3137,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 +3255,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();
},
@@ -3214,6 +3270,14 @@ BrowserGlue.prototype = {
function reportInstallationTelemetry() {
lazy.BrowserUsageTelemetry.reportInstallationTelemetry();
},
+
+ function trustObjectTelemetry() {
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ // countTrustObjects also logs the number of trust objects for telemetry purposes
+ certdb.countTrustObjects();
+ },
];
for (let task of idleTasks) {
@@ -3695,11 +3759,11 @@ BrowserGlue.prototype = {
_onThisDeviceConnected() {
const [title, body] = lazy.accountsL10n.formatValuesSync([
- "account-connection-title",
+ "account-connection-title-2",
"account-connection-connected",
]);
- let clickCallback = (subject, topic, data) => {
+ let clickCallback = (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
@@ -3740,7 +3804,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 = 147;
const BROWSER_DOCURL = AppConstants.BROWSER_CHROME_URL;
if (!Services.prefs.prefHasUserValue("browser.migration.version")) {
@@ -3748,12 +3812,6 @@ BrowserGlue.prototype = {
Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
this._isNewProfile = true;
- if (AppConstants.platform == "win") {
- // Ensure that the Firefox Bridge protocols are registered for the new profile.
- // No-op if they are registered for the user or the local machine already.
- lazy.FirefoxBridgeExtensionUtils.maybeRegisterFirefoxBridgeProtocols();
- }
-
return;
}
@@ -4354,14 +4412,35 @@ BrowserGlue.prototype = {
}
if (currentUIVersion < 143) {
+ // Version 143 has been superseded by version 145 below.
+ }
+
+ 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);
+ }
+ }
+
+ if (currentUIVersion < 145) {
if (AppConstants.platform == "win") {
// In Firefox 122, we enabled the firefox and firefox-private protocols.
// We switched over to using firefox-bridge and firefox-private-bridge,
// but we want to clean up the use of the other protocols.
- lazy.FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries();
-
- // Register the new firefox bridge related protocols now
- lazy.FirefoxBridgeExtensionUtils.maybeRegisterFirefoxBridgeProtocols();
+ lazy.FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
+ lazy.FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL,
+ lazy.FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL
+ );
// Clean up the old user prefs from FX 122
Services.prefs.clearUserPref(
@@ -4370,10 +4449,82 @@ BrowserGlue.prototype = {
Services.prefs.clearUserPref(
"network.protocol-handler.external.firefox-private"
);
+
+ // In Firefox 126, we switched over to using native messaging so the
+ // protocols are no longer necessary even in firefox-bridge and
+ // firefox-private-bridge form
+ lazy.FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
+ lazy.FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL,
+ lazy.FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL
+ );
+ Services.prefs.clearUserPref(
+ "network.protocol-handler.external.firefox-bridge"
+ );
+ Services.prefs.clearUserPref(
+ "network.protocol-handler.external.firefox-private-bridge"
+ );
Services.prefs.clearUserPref("browser.shell.customProtocolsRegistered");
}
}
+ // Version 146 had a typo issue and thus it has been replaced by 147.
+
+ if (currentUIVersion < 147) {
+ // We're securing the boolean prefs for OS Authentication.
+ // This is achieved by converting them into a string pref and encrypting the values
+ // stored inside it.
+
+ if (!AppConstants.NIGHTLY_BUILD) {
+ const hasRunBetaMigration = Services.prefs
+ .getCharPref("browser.startup.homepage_override.mstone", "")
+ .startsWith("127.0");
+
+ // Version 146 UI migration wrote to a wrong `creditcards` pref when
+ // the feature was disabled, instead it should have used `creditCards`.
+ // The correct pref name is in AUTOFILL_CREDITCARDS_REAUTH_PREF.
+ // Note that we only wrote prefs if the feature was disabled.
+ let ccTypoDisabled = !lazy.FormAutofillUtils.getOSAuthEnabled(
+ "extensions.formautofill.creditcards.reauth.optout"
+ );
+ let ccCorrectPrefDisabled = !lazy.FormAutofillUtils.getOSAuthEnabled(
+ lazy.FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ );
+ let ccPrevReauthPrefValue = Services.prefs.getBoolPref(
+ "extensions.formautofill.reauth.enabled",
+ false
+ );
+
+ let userHadEnabledCreditCardReauth =
+ // If we've run beta migration, and neither typo nor correct pref
+ // indicate disablement, the user enabled the pref:
+ (hasRunBetaMigration && !ccTypoDisabled && !ccCorrectPrefDisabled) ||
+ // Or if we never ran beta migration and the bool pref is set:
+ ccPrevReauthPrefValue;
+
+ lazy.FormAutofillUtils.setOSAuthEnabled(
+ lazy.FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ userHadEnabledCreditCardReauth
+ );
+
+ if (!hasRunBetaMigration) {
+ const passwordsPrevReauthPrefValue = Services.prefs.getBoolPref(
+ "signon.management.page.os-auth.enabled",
+ false
+ );
+ lazy.LoginHelper.setOSAuthEnabled(
+ lazy.LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF,
+ passwordsPrevReauthPrefValue
+ );
+ }
+ }
+
+ Services.prefs.clearUserPref("extensions.formautofill.reauth.enabled");
+ Services.prefs.clearUserPref("signon.management.page.os-auth.enabled");
+ Services.prefs.clearUserPref(
+ "extensions.formautofill.creditcards.reauth.optout"
+ );
+ }
+
// Update the migration version.
Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
},
@@ -4462,14 +4613,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";
}
@@ -4477,10 +4620,7 @@ BrowserGlue.prototype = {
return "disallow-postUpdate";
}
- const useMROnboarding =
- lazy.NimbusFeatures.majorRelease2022.getVariable("onboarding");
const showUpgradeDialog =
- useMROnboarding ??
lazy.NimbusFeatures.upgradeDialog.getVariable("enabled");
return showUpgradeDialog ? "" : "disabled";
@@ -4712,7 +4852,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;
}
@@ -4736,6 +4876,32 @@ BrowserGlue.prototype = {
}
},
+ async _onIncomingCloseTabCommand(data) {
+ // The payload is wrapped weirdly because of how Sync does notifications.
+ const wrappedObj = data.wrappedJSObject.object;
+ let { urls } = wrappedObj[0];
+ let urisToClose = [];
+ urls.forEach(urlString => {
+ try {
+ urisToClose.push(Services.io.newURI(urlString));
+ } catch (ex) {
+ // The url was invalid so we ignore
+ console.error(ex);
+ }
+ });
+ for (let win of lazy.BrowserWindowTracker.orderedWindows) {
+ // Ensure we're operating on fully opened browser windows
+ if (!win.gBrowser) {
+ continue;
+ }
+ urisToClose = await win.gBrowser.closeTabsByURI(urisToClose);
+ // If we've successfully closed all the tabs, break early
+ if (!urisToClose.length) {
+ break;
+ }
+ }
+ },
+
async _onVerifyLoginNotification({ body, title, url }) {
let tab;
let imageURL;
@@ -4751,7 +4917,7 @@ BrowserGlue.prototype = {
tab = win.gBrowser.addWebTab(url);
}
tab.attention = true;
- let clickCallback = (subject, topic, data) => {
+ let clickCallback = (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
@@ -4774,13 +4940,13 @@ BrowserGlue.prototype = {
_onDeviceConnected(deviceName) {
const [title, body] = lazy.accountsL10n.formatValuesSync([
- { id: "account-connection-title" },
+ { id: "account-connection-title-2" },
deviceName
? { id: "account-connection-connected-with", args: { deviceName } }
: { id: "account-connection-connected-with-noname" },
]);
- let clickCallback = async (subject, topic, data) => {
+ let clickCallback = async (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
@@ -4811,11 +4977,11 @@ BrowserGlue.prototype = {
_onDeviceDisconnected() {
const [title, body] = lazy.accountsL10n.formatValuesSync([
- "account-connection-title",
+ "account-connection-title-2",
"account-connection-disconnected",
]);
- let clickCallback = (subject, topic, data) => {
+ let clickCallback = (subject, topic) => {
if (topic != "alertclickcallback") {
return;
}
@@ -4870,7 +5036,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);
@@ -5694,7 +5860,9 @@ export var AboutHomeStartupCache = {
this.setDeferredResult(this.CACHE_RESULT_SCALARS.UNSET);
- this._enabled = !!lazy.NimbusFeatures.abouthomecache.getVariable("enabled");
+ this._enabled = Services.prefs.getBoolPref(
+ "browser.startup.homepage.abouthome_cache.enabled"
+ );
if (!this._enabled) {
this.recordResult(this.CACHE_RESULT_SCALARS.DISABLED);
@@ -6475,11 +6643,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/StartupRecorder.sys.mjs b/browser/components/StartupRecorder.sys.mjs
index 7b69b52690..bdc08ee6dd 100644
--- a/browser/components/StartupRecorder.sys.mjs
+++ b/browser/components/StartupRecorder.sys.mjs
@@ -6,6 +6,23 @@ const Cm = Components.manager;
Cm.QueryInterface(Ci.nsIServiceManager);
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "BROWSER_STARTUP_RECORD",
+ "browser.startup.record",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "BROWSER_STARTUP_RECORD_IMAGES",
+ "browser.startup.recordImages",
+ false
+);
let firstPaintNotification = "widget-first-paint";
// widget-first-paint fires much later than expected on Linux.
@@ -98,10 +115,7 @@ StartupRecorder.prototype = {
return;
}
- if (
- !Services.prefs.getBoolPref("browser.startup.record", false) &&
- !Services.prefs.getBoolPref("browser.startup.recordImages", false)
- ) {
+ if (!lazy.BROWSER_STARTUP_RECORD && !lazy.BROWSER_STARTUP_RECORD_IMAGES) {
this._resolve();
this._resolve = null;
return;
@@ -118,7 +132,7 @@ StartupRecorder.prototype = {
"browser-startup-idle-tasks-finished",
];
- if (Services.prefs.getBoolPref("browser.startup.recordImages", false)) {
+ if (lazy.BROWSER_STARTUP_RECORD_IMAGES) {
// For code simplicify, recording images excludes the other startup
// recorder behaviors, so we can observe only the image topics.
topics = [
@@ -180,7 +194,7 @@ StartupRecorder.prototype = {
this.record.bind(this, "before handling user events")
);
} else if (topic == "browser-startup-idle-tasks-finished") {
- if (Services.prefs.getBoolPref("browser.startup.recordImages", false)) {
+ if (lazy.BROWSER_STARTUP_RECORD_IMAGES) {
Services.obs.removeObserver(this, "image-drawing");
Services.obs.removeObserver(this, "image-loading");
this._resolve();
diff --git a/browser/components/aboutlogins/AboutLoginsChild.sys.mjs b/browser/components/aboutlogins/AboutLoginsChild.sys.mjs
index 3fcdf77923..c7059d8f40 100644
--- a/browser/components/aboutlogins/AboutLoginsChild.sys.mjs
+++ b/browser/components/aboutlogins/AboutLoginsChild.sys.mjs
@@ -160,7 +160,11 @@ export class AboutLoginsChild extends JSWindowActorChild {
}
#aboutLoginsCopyLoginDetail(detail) {
- lazy.ClipboardHelper.copyString(detail, lazy.ClipboardHelper.Sensitive);
+ lazy.ClipboardHelper.copyString(
+ detail,
+ this.windowContext,
+ lazy.ClipboardHelper.Sensitive
+ );
}
#aboutLoginsCreateLogin(login) {
diff --git a/browser/components/aboutlogins/AboutLoginsParent.sys.mjs b/browser/components/aboutlogins/AboutLoginsParent.sys.mjs
index 28f56c2172..4342e60296 100644
--- a/browser/components/aboutlogins/AboutLoginsParent.sys.mjs
+++ b/browser/components/aboutlogins/AboutLoginsParent.sys.mjs
@@ -17,7 +17,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
LoginExport: "resource://gre/modules/LoginExport.sys.mjs",
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
- OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
UIState: "resource://services-sync/UIState.sys.mjs",
});
@@ -38,12 +37,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
- "OS_AUTH_ENABLED",
- "signon.management.page.os-auth.enabled",
- true
-);
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
"VULNERABLE_PASSWORDS_ENABLED",
"signon.management.page.vulnerable-passwords.enabled",
false
@@ -266,11 +259,15 @@ export class AboutLoginsParent extends JSWindowActorParent {
let messageText = { value: "NOT SUPPORTED" };
let captionText = { value: "" };
+ const isOSAuthEnabled = lazy.LoginHelper.getOSAuthEnabled(
+ lazy.LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF
+ );
+
// This feature is only supported on Windows and macOS
// but we still call in to OSKeyStore on Linux to get
// the proper auth_details for Telemetry.
// See bug 1614874 for Linux support.
- if (lazy.OS_AUTH_ENABLED && lazy.OSKeyStore.canReauth()) {
+ if (isOSAuthEnabled) {
messageId += "-" + AppConstants.platform;
[messageText, captionText] = await lazy.AboutLoginsL10n.formatMessages([
{
@@ -284,7 +281,7 @@ export class AboutLoginsParent extends JSWindowActorParent {
let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
this.browsingContext.embedderElement,
- lazy.OS_AUTH_ENABLED,
+ isOSAuthEnabled,
AboutLogins._authExpirationTime,
messageText.value,
captionText.value
@@ -378,11 +375,15 @@ export class AboutLoginsParent extends JSWindowActorParent {
let messageText = { value: "NOT SUPPORTED" };
let captionText = { value: "" };
+ const isOSAuthEnabled = lazy.LoginHelper.getOSAuthEnabled(
+ lazy.LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF
+ );
+
// This feature is only supported on Windows and macOS
// but we still call in to OSKeyStore on Linux to get
// the proper auth_details for Telemetry.
// See bug 1614874 for Linux support.
- if (lazy.OSKeyStore.canReauth()) {
+ if (isOSAuthEnabled) {
const messageId =
EXPORT_PASSWORD_OS_AUTH_DIALOG_MESSAGE_IDS[AppConstants.platform];
if (!messageId) {
diff --git a/browser/components/aboutlogins/LoginBreaches.sys.mjs b/browser/components/aboutlogins/LoginBreaches.sys.mjs
index b2a0af5e39..496e5de575 100644
--- a/browser/components/aboutlogins/LoginBreaches.sys.mjs
+++ b/browser/components/aboutlogins/LoginBreaches.sys.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/. */
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
/**
* Manages breach alerts for saved logins using data from Firefox Monitor via
* RemoteSettings.
@@ -16,6 +18,13 @@ ChromeUtils.defineESModuleGetters(lazy, {
"resource://services-settings/RemoteSettingsClient.sys.mjs",
});
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "VULNERABLE_PASSWORDS_ENABLED",
+ "signon.management.page.vulnerable-passwords.enabled",
+ false
+);
+
export const LoginBreaches = {
REMOTE_SETTINGS_COLLECTION: "fxmonitor-breaches",
@@ -138,6 +147,20 @@ export const LoginBreaches = {
return vulnerablePasswordsByLoginGUID;
},
+ recordBreachAlertDismissal(loginGuid) {
+ const storageJSON = Services.logins.wrappedJSObject._storage;
+ return storageJSON.recordBreachAlertDismissal(loginGuid);
+ },
+
+ isVulnerablePassword(login) {
+ if (!lazy.VULNERABLE_PASSWORDS_ENABLED) {
+ return false;
+ }
+
+ const storageJSON = Services.logins.wrappedJSObject._storage;
+ return storageJSON.isPotentiallyVulnerablePassword(login);
+ },
+
async clearAllPotentiallyVulnerablePasswords() {
await Services.logins.initializationPromise;
const storageJSON = Services.logins.wrappedJSObject._storage;
diff --git a/browser/components/aboutlogins/content/aboutLogins.html b/browser/components/aboutlogins/content/aboutLogins.html
index 67712c8f29..a0c04149e7 100644
--- a/browser/components/aboutlogins/content/aboutLogins.html
+++ b/browser/components/aboutlogins/content/aboutLogins.html
@@ -11,7 +11,6 @@
<title data-l10n-id="about-logins-page-title-name"></title>
<link rel="localization" href="branding/brand.ftl">
<link rel="localization" href="browser/aboutLogins.ftl">
- <link rel="localization" href="toolkit/branding/accounts.ftl">
<link rel="localization" href="toolkit/branding/brandings.ftl">
<script type="module" src="chrome://browser/content/aboutlogins/components/confirmation-dialog.mjs"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/remove-logins-dialog.mjs"></script>
diff --git a/browser/components/aboutlogins/content/aboutLoginsImportReport.html b/browser/components/aboutlogins/content/aboutLoginsImportReport.html
index 9ab2641ca2..c9956e2cca 100644
--- a/browser/components/aboutlogins/content/aboutLoginsImportReport.html
+++ b/browser/components/aboutlogins/content/aboutLoginsImportReport.html
@@ -14,7 +14,6 @@
<title data-l10n-id="about-logins-import-report-page-title"></title>
<link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="browser/aboutLogins.ftl" />
- <link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<script
type="module"
diff --git a/browser/components/aboutlogins/content/components/login-item.css b/browser/components/aboutlogins/content/components/login-item.css
index 4a3d85d859..e9e91f78ed 100644
--- a/browser/components/aboutlogins/content/components/login-item.css
+++ b/browser/components/aboutlogins/content/components/login-item.css
@@ -64,11 +64,6 @@ form {
display: none;
}
-input[type="password"],
-input[type="text"],
-input[type="url"] {
- text-align: match-parent !important; /* override `all: unset` in the rule below */
-}
:host(:not([data-editing])) input[type="password"]:read-only,
input[type="text"]:read-only,
@@ -82,6 +77,17 @@ input[type="url"]:read-only {
width: 100%;
}
+input:is([type="password"], [type="text"], [type="url"]) {
+ /* Override all: unset above */
+ appearance: textfield !important;
+ text-align: match-parent !important;
+}
+
+input.password-display,
+input[name="password"] {
+ font-family: monospace !important; /* Override all: unset above */
+}
+
/* We can't use `margin-inline-start` here because we force
* the input to have dir="ltr", so we set the margin manually
* using the parent element's directionality. */
@@ -197,11 +203,6 @@ moz-button-group,
box-shadow: none;
}
-input.password-display,
-input[name="password"] {
- font-family: monospace !important; /* override `all: unset` in the rule above */
-}
-
.reveal-password-checkbox {
appearance: none;
background-image: url("resource://gre-resources/password.svg");
diff --git a/browser/components/aboutlogins/tests/browser/browser.toml b/browser/components/aboutlogins/tests/browser/browser.toml
index 0b38e0dda1..07c49c4c88 100644
--- a/browser/components/aboutlogins/tests/browser/browser.toml
+++ b/browser/components/aboutlogins/tests/browser/browser.toml
@@ -2,7 +2,6 @@
support-files = ["head.js"]
prefs = [
"signon.management.page.vulnerable-passwords.enabled=true",
- "signon.management.page.os-auth.enabled=true",
"toolkit.telemetry.ipcBatchTimeout=10", # lower the interval for event telemetry in the content process to update the parent process
]
# Run first so content events from previous tests won't trickle in.
diff --git a/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js b/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js
index 52b2eb02a2..4f3cdbe48e 100644
--- a/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js
+++ b/browser/components/aboutlogins/tests/browser/browser_aaa_eventTelemetry_run_first.js
@@ -72,7 +72,10 @@ add_task(async function test_telemetry_events() {
await LoginTestUtils.telemetry.waitForEventCount(3);
if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
- let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let reauthObserved = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
let loginItem = content.document.querySelector("login-item");
let copyButton = loginItem.shadowRoot.querySelector(
@@ -106,9 +109,12 @@ add_task(async function test_telemetry_events() {
// Show the password
if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
- let reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
- loginResult: true,
- });
+ let reauthObserved = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ }
nextTelemetryEventCount++; // An extra event is observed for the reauth event.
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
let loginItem = content.document.querySelector("login-item");
diff --git a/browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js b/browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js
index b2b036121a..b28dcf25ee 100644
--- a/browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js
+++ b/browser/components/aboutlogins/tests/browser/browser_alertDismissedAfterChangingPassword.js
@@ -32,6 +32,7 @@ add_setup(async function () {
gBrowser,
url: "about:logins",
});
+
registerCleanupFunction(() => {
BrowserTestUtils.removeTab(gBrowser.selectedTab);
Services.logins.removeAllUserFacingLogins();
@@ -104,9 +105,13 @@ add_task(async function test_added_login_shows_breach_warning() {
return;
}
- let reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
- loginResult: true,
- });
+ let reauthObserved = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ }
+
// Change the password on the breached login and check that the
// login is no longer marked as breached. The vulnerable login
// should still be marked as vulnerable afterwards.
diff --git a/browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js b/browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js
index ee1527d792..55180435c7 100644
--- a/browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js
+++ b/browser/components/aboutlogins/tests/browser/browser_contextmenuFillLogins.js
@@ -32,8 +32,10 @@ if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
gTests[gTests.length] = {
name: "test contextmenu on password field in edit login view",
async setup(browser) {
- let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
-
+ let osAuthDialogShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
// load up the edit login view
await SpecialPowers.spawn(
browser,
diff --git a/browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js b/browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js
index 8b0d3b7bf6..1f6eff8806 100644
--- a/browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js
+++ b/browser/components/aboutlogins/tests/browser/browser_copyToClipboardButton.js
@@ -49,9 +49,11 @@ add_task(async function test() {
info(
"waiting for " + testObj.expectedValue + " to be placed on clipboard"
);
- let reauthObserved = true;
+ let reauthObserved = Promise.resolve();
if (testObj.copyButtonSelector.includes("password")) {
- reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
}
await SimpleTest.promiseClipboardChange(
diff --git a/browser/components/aboutlogins/tests/browser/browser_createLogin.js b/browser/components/aboutlogins/tests/browser/browser_createLogin.js
index 4aac8cece1..46c4487ba3 100644
--- a/browser/components/aboutlogins/tests/browser/browser_createLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_createLogin.js
@@ -232,9 +232,12 @@ add_task(async function test_create_login() {
continue;
}
- let reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
- loginResult: true,
- });
+ let reauthObserved = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ }
await SpecialPowers.spawn(browser, [], async () => {
let loginItem = Cu.waiveXrays(
content.document.querySelector("login-item")
diff --git a/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js b/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
index 90195d0e0a..4fc250523c 100644
--- a/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_deleteLogin.js
@@ -71,7 +71,10 @@ add_task(async function test_login_item() {
}, "Waiting for login item to get populated");
Assert.ok(loginItemPopulated, "The login item should get populated");
});
- let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let reauthObserved = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(browser, [], async () => {
let loginItem = Cu.waiveXrays(
content.document.querySelector("login-item")
diff --git a/browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js b/browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js
index a78f49d3c9..1789d47b8e 100644
--- a/browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js
+++ b/browser/components/aboutlogins/tests/browser/browser_loginItemErrors.js
@@ -107,7 +107,10 @@ add_task(async function test_showLoginItemErrors() {
// The rest of the test uses Edit mode which causes an OS prompt in official builds.
return;
}
- let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let reauthObserved = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(
browser,
[[LoginHelper.loginToVanillaObject(LOGIN_TO_UPDATE), LOGIN_UPDATES]],
diff --git a/browser/components/aboutlogins/tests/browser/browser_openExport.js b/browser/components/aboutlogins/tests/browser/browser_openExport.js
index 1a61510862..f4f7761259 100644
--- a/browser/components/aboutlogins/tests/browser/browser_openExport.js
+++ b/browser/components/aboutlogins/tests/browser/browser_openExport.js
@@ -8,9 +8,6 @@
* Test the export logins file picker appears.
*/
-let { OSKeyStore } = ChromeUtils.importESModule(
- "resource://gre/modules/OSKeyStore.sys.mjs"
-);
let { TelemetryTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TelemetryTestUtils.sys.mjs"
);
diff --git a/browser/components/aboutlogins/tests/browser/browser_openSite.js b/browser/components/aboutlogins/tests/browser/browser_openSite.js
index f33d57a8e4..c3fc8d5cd1 100644
--- a/browser/components/aboutlogins/tests/browser/browser_openSite.js
+++ b/browser/components/aboutlogins/tests/browser/browser_openSite.js
@@ -44,7 +44,10 @@ add_task(async function test_launch_login_item() {
gBrowser,
TEST_LOGIN1.origin + "/"
);
- let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let reauthObserved = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(browser, [], async () => {
let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
loginItem._editButton.click();
diff --git a/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js b/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
index 9c2688cc77..21a5eeda12 100644
--- a/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
+++ b/browser/components/aboutlogins/tests/browser/browser_osAuthDialog.js
@@ -1,12 +1,105 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
-add_task(async function test() {
- info(
- `updatechannel: ${UpdateUtils.getUpdateChannel(false)}; platform: ${
- AppConstants.platform
- }`
+"use strict";
+
+// On mac, this test times out in chaos mode
+requestLongerTimeout(2);
+
+const PAGE_PREFS = "about:preferences";
+const PAGE_PRIVACY = PAGE_PREFS + "#privacy";
+const SELECTORS = {
+ reauthCheckbox: "#osReauthCheckbox",
+};
+
+add_setup(async function () {
+ TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
+ TEST_LOGIN2 = await addLogin(TEST_LOGIN2);
+ // Undo mocking from head.js
+ sinon.restore();
+});
+
+add_task(async function test_os_auth_enabled_with_checkbox() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+
+ await SpecialPowers.spawn(
+ browser,
+ [SELECTORS, AppConstants.NIGHTLY_BUILD],
+ async (selectors, isNightly) => {
+ is(
+ content.document.querySelector(selectors.reauthCheckbox).checked,
+ isNightly,
+ "OSReauth for Passwords should be checked"
+ );
+ }
+ );
+ is(
+ LoginHelper.getOSAuthEnabled(PASSWORDS_OS_REAUTH_PREF),
+ AppConstants.NIGHTLY_BUILD,
+ "OSAuth should be enabled."
+ );
+ }
);
+});
+
+add_task(async function test_os_auth_disabled_with_checkbox() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ LoginHelper.setOSAuthEnabled(PASSWORDS_OS_REAUTH_PREF, false);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+
+ await SpecialPowers.spawn(browser, [SELECTORS], async selectors => {
+ is(
+ content.document.querySelector(selectors.reauthCheckbox).checked,
+ false,
+ "OSReauth for passwords should be unchecked"
+ );
+ });
+ is(
+ LoginHelper.getOSAuthEnabled(PASSWORDS_OS_REAUTH_PREF),
+ false,
+ "OSAuth should be disabled"
+ );
+ }
+ );
+ LoginHelper.setOSAuthEnabled(PASSWORDS_OS_REAUTH_PREF, true);
+});
+
+add_task(async function test_OSAuth_enabled_with_random_value_in_pref() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ await SpecialPowers.pushPrefEnv({
+ set: [[PASSWORDS_OS_REAUTH_PREF, "poutine-gravy"]],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], async selectors => {
+ let reauthCheckbox = content.document.querySelector(
+ selectors.reauthCheckbox
+ );
+ is(
+ reauthCheckbox.checked,
+ true,
+ "OSReauth for passwords should be checked"
+ );
+ });
+ is(
+ LoginHelper.getOSAuthEnabled(PASSWORDS_OS_REAUTH_PREF),
+ true,
+ "OSAuth should be enabled since the pref does not decrypt to 'opt out'."
+ );
+ }
+ );
+});
+
+add_task(async function test_osAuth_shown_on_edit_login() {
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
Assert.ok(
true,
@@ -14,41 +107,51 @@ add_task(async function test() {
);
return;
}
-
- TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
-
await BrowserTestUtils.openNewForegroundTab({
gBrowser,
url: "about:logins",
});
-
- registerCleanupFunction(function () {
- Services.logins.removeAllUserFacingLogins();
- BrowserTestUtils.removeTab(gBrowser.selectedTab);
- });
-
- // Show OS auth dialog when Reveal Password checkbox is checked if not on a new login
- let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false);
+ let osAuthDialogShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
let loginItem = content.document.querySelector("login-item");
- let revealCheckbox = loginItem.shadowRoot.querySelector(
- ".reveal-password-checkbox"
+ Assert.ok(
+ !loginItem.dataset.editing,
+ "Not in edit mode before clicking 'Edit'"
);
- revealCheckbox.click();
+ let editButton = loginItem.shadowRoot.querySelector("edit-button");
+ editButton.click();
});
+
await osAuthDialogShown;
- info("OS auth dialog shown and canceled");
- await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
- let loginItem = content.document.querySelector("login-item");
- let revealCheckbox = loginItem.shadowRoot.querySelector(
- ".reveal-password-checkbox"
+ info("OS auth dialog shown and authenticated");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector("login-item").dataset.editing,
+ "login item should be in 'edit' mode"
);
+ });
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_osAuth_shown_on_reveal_password() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
Assert.ok(
- !revealCheckbox.checked,
- "reveal checkbox should be unchecked if OS auth dialog canceled"
+ true,
+ `skipping test since oskeystore cannot be automated in this environment`
);
+ return;
+ }
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
});
- osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let osAuthDialogShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
let loginItem = content.document.querySelector("login-item");
let revealCheckbox = loginItem.shadowRoot.querySelector(
@@ -68,10 +171,70 @@ add_task(async function test() {
"reveal checkbox should be checked if OS auth dialog authenticated"
);
});
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
- info("'Edit' shouldn't show the prompt since the user has authenticated now");
+add_task(async function test_osAuth_shown_on_copy_password() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ Assert.ok(
+ true,
+ `skipping test since oskeystore cannot be automated in this environment`
+ );
+ return;
+ }
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ let osAuthDialogShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
let loginItem = content.document.querySelector("login-item");
+ let copyPassword = loginItem.shadowRoot.querySelector(
+ "copy-password-button"
+ );
+ copyPassword.click();
+ });
+ await osAuthDialogShown;
+ info("OS auth dialog shown and authenticated");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ info("Password was copied to clipboard");
+ });
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_osAuth_not_shown_within_expiration_time() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ Assert.ok(
+ true,
+ `skipping test since oskeystore cannot be automated in this environment`
+ );
+ return;
+ }
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ let osAuthDialogShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let copyPassword = loginItem.shadowRoot.querySelector(
+ "copy-password-button"
+ );
+ copyPassword.click();
+ });
+ await osAuthDialogShown;
+ info("OS auth dialog shown and authenticated");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ info(
+ "'Edit' shouldn't show the prompt since the user has authenticated now"
+ );
+ let loginItem = content.document.querySelector("login-item");
Assert.ok(
!loginItem.dataset.editing,
"Not in edit mode before clicking 'Edit'"
@@ -85,16 +248,43 @@ add_task(async function test() {
);
Assert.ok(loginItem.dataset.editing, "In edit mode");
});
-
- info("Test that the OS auth prompt is shown after about:logins is reopened");
BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_osAuth_shown_after_expiration_timeout() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ Assert.ok(
+ true,
+ `skipping test since oskeystore cannot be automated in this environment`
+ );
+ return;
+ }
await BrowserTestUtils.openNewForegroundTab({
gBrowser,
url: "about:logins",
});
+ let osAuthDialogShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let copyPassword = loginItem.shadowRoot.querySelector(
+ "copy-password-button"
+ );
+ copyPassword.click();
+ });
+ await osAuthDialogShown;
+ info("OS auth dialog shown and authenticated");
+
+ // Show OS auth dialog since the timeout will have expired
+
+ if (OSKeyStore.canReauth()) {
+ osAuthDialogShown = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ }
- // Show OS auth dialog since the page has been reloaded.
- osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false);
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
let loginItem = content.document.querySelector("login-item");
let revealCheckbox = loginItem.shadowRoot.querySelector(
@@ -103,63 +293,91 @@ add_task(async function test() {
revealCheckbox.click();
});
await osAuthDialogShown;
- info("OS auth dialog shown and canceled");
+ info("OS auth dialog shown and authenticated");
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
- // Show OS auth dialog since the previous attempt was canceled
- osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+add_task(async function test_osAuth_shown_on_reload() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ Assert.ok(
+ true,
+ `skipping test since oskeystore cannot be automated in this environment`
+ );
+ return;
+ }
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
+ });
+ let osAuthDialogShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
let loginItem = content.document.querySelector("login-item");
- let revealCheckbox = loginItem.shadowRoot.querySelector(
- ".reveal-password-checkbox"
+ let copyPassword = loginItem.shadowRoot.querySelector(
+ "copy-password-button"
);
- revealCheckbox.click();
- info("clicking on reveal checkbox to hide the password");
- revealCheckbox.click();
+ copyPassword.click();
});
await osAuthDialogShown;
- info("OS auth dialog shown and passed");
+ info("OS auth dialog shown and authenticated");
- // Show OS auth dialog since the timeout will have expired
- osAuthDialogShown = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
- loginResult: true,
+ info("Test that the OS auth prompt is shown after about:logins is reopened");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:logins",
});
+
+ // Show OS auth dialog since the page has been reloaded.
+ osAuthDialogShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
let loginItem = content.document.querySelector("login-item");
let revealCheckbox = loginItem.shadowRoot.querySelector(
".reveal-password-checkbox"
);
- info("clicking on reveal checkbox to reveal password");
revealCheckbox.click();
});
- info("waiting for os auth dialog");
await osAuthDialogShown;
- info("OS auth dialog shown and passed after timeout expiration");
-
- // Disable the OS auth feature and confirm the prompt doesn't appear
- await SpecialPowers.pushPrefEnv({
- set: [["signon.management.page.os-auth.enabled", false]],
- });
- info("Reload about:logins to reset the timeout");
+ info("OS auth dialog shown and authenticated");
BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function test_osAuth_shown_again_on_cancel() {
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ Assert.ok(
+ true,
+ `skipping test since oskeystore cannot be automated in this environment`
+ );
+ return;
+ }
await BrowserTestUtils.openNewForegroundTab({
gBrowser,
url: "about:logins",
});
-
- info("'Edit' shouldn't show the prompt since the feature has been disabled");
+ let osAuthDialogShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false);
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
let loginItem = content.document.querySelector("login-item");
- Assert.ok(
- !loginItem.dataset.editing,
- "Not in edit mode before clicking 'Edit'"
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
);
- let editButton = loginItem.shadowRoot.querySelector("edit-button");
- editButton.click();
-
- await ContentTaskUtils.waitForCondition(
- () => loginItem.dataset.editing,
- "waiting for 'edit' mode"
+ revealCheckbox.click();
+ });
+ await osAuthDialogShown;
+ info("OS auth dialog shown and canceled");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ let loginItem = content.document.querySelector("login-item");
+ let revealCheckbox = loginItem.shadowRoot.querySelector(
+ ".reveal-password-checkbox"
+ );
+ Assert.ok(
+ !revealCheckbox.checked,
+ "reveal checkbox should be unchecked if OS auth dialog canceled"
);
- Assert.ok(loginItem.dataset.editing, "In edit mode");
});
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
diff --git a/browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js b/browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js
index c5879ceeaf..bcd09ca9a2 100644
--- a/browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js
+++ b/browser/components/aboutlogins/tests/browser/browser_removeAllDialog.js
@@ -2,8 +2,6 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
-const OS_REAUTH_PREF = "signon.management.page.os-auth.enabled";
-
async function openRemoveAllDialog(browser) {
await SimpleTest.promiseFocus(browser);
await BrowserTestUtils.synthesizeMouseAtCenter("menu-button", {}, browser);
@@ -80,9 +78,11 @@ async function waitForRemoveAllLogins() {
}
add_setup(async function () {
- await SpecialPowers.pushPrefEnv({
- set: [[OS_REAUTH_PREF, false]],
- });
+ // Undo mocking from head.js
+ sinon.restore();
+
+ let oldPrefValue = LoginHelper.getOSAuthEnabled(PASSWORDS_OS_REAUTH_PREF);
+ LoginHelper.setOSAuthEnabled(PASSWORDS_OS_REAUTH_PREF, false);
await BrowserTestUtils.openNewForegroundTab({
gBrowser,
url: "about:logins",
@@ -90,7 +90,7 @@ add_setup(async function () {
registerCleanupFunction(async () => {
BrowserTestUtils.removeTab(gBrowser.selectedTab);
Services.logins.removeAllUserFacingLogins();
- await SpecialPowers.popPrefEnv();
+ LoginHelper.setOSAuthEnabled(PASSWORDS_OS_REAUTH_PREF, oldPrefValue);
});
TEST_LOGIN1 = await addLogin(TEST_LOGIN1);
});
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/browser_tabKeyNav.js b/browser/components/aboutlogins/tests/browser/browser_tabKeyNav.js
index 890d39a316..6e2047e8e5 100644
--- a/browser/components/aboutlogins/tests/browser/browser_tabKeyNav.js
+++ b/browser/components/aboutlogins/tests/browser/browser_tabKeyNav.js
@@ -100,14 +100,6 @@ add_task(async function test_tab_key_nav() {
expectedSelector
);
- // By default, MacOS will skip over certain text controls, such as links.
- if (
- content.window.navigator.platform.toLowerCase().includes("mac") &&
- expectedElement.tagName === "A"
- ) {
- continue;
- }
-
const actualElement = getFocusedElement();
Assert.equal(
@@ -126,13 +118,6 @@ add_task(async function test_tab_key_nav() {
content.document,
expectedSelector
);
- // By default, MacOS will skip over certain text controls, such as links.
- if (
- content.window.navigator.platform.toLowerCase().includes("mac") &&
- expectedElement.tagName === "A"
- ) {
- continue;
- }
const actualElement = getFocusedElement();
Assert.equal(
diff --git a/browser/components/aboutlogins/tests/browser/browser_updateLogin.js b/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
index 686b3951a1..192ff0270d 100644
--- a/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
+++ b/browser/components/aboutlogins/tests/browser/browser_updateLogin.js
@@ -124,7 +124,10 @@ add_task(async function test_login_item() {
}
let browser = gBrowser.selectedBrowser;
- let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let reauthObserved = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await SpecialPowers.spawn(
browser,
[LoginHelper.loginToVanillaObject(TEST_LOGIN1)],
@@ -163,9 +166,11 @@ add_task(async function test_login_item() {
],
test_discard_dialog
);
- reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
- loginResult: true,
- });
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ }
await SpecialPowers.spawn(browser, [], async () => {
let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
let editButton = loginItem.shadowRoot
@@ -184,9 +189,11 @@ add_task(async function test_login_item() {
],
test_discard_dialog
);
- reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
- loginResult: true,
- });
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ }
await SpecialPowers.spawn(browser, [], async () => {
let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
let editButton = loginItem.shadowRoot
@@ -289,9 +296,11 @@ add_task(async function test_login_item() {
);
}
);
- reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
- loginResult: true,
- });
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ }
await SpecialPowers.spawn(browser, [], async () => {
let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
let editButton = loginItem.shadowRoot
@@ -360,9 +369,11 @@ add_task(async function test_login_item() {
"Password field width should be correctly updated"
);
});
- reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
- loginResult: true,
- });
+ if (OSKeyStore.canReauth()) {
+ reauthObserved = forceAuthTimeoutAndWaitForOSKeyStoreLogin({
+ loginResult: true,
+ });
+ }
await SpecialPowers.spawn(browser, [], async () => {
let loginItem = Cu.waiveXrays(content.document.querySelector("login-item"));
let editButton = loginItem.shadowRoot
diff --git a/browser/components/aboutlogins/tests/browser/head.js b/browser/components/aboutlogins/tests/browser/head.js
index 2aec0e632a..22ab7ef964 100644
--- a/browser/components/aboutlogins/tests/browser/head.js
+++ b/browser/components/aboutlogins/tests/browser/head.js
@@ -13,6 +13,24 @@ let { _AboutLogins } = ChromeUtils.importESModule(
let { OSKeyStoreTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/OSKeyStoreTestUtils.sys.mjs"
);
+
+const { OSKeyStore } = ChromeUtils.importESModule(
+ "resource://gre/modules/OSKeyStore.sys.mjs"
+);
+
+let { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+// Always pretend OS Auth is enabled in this dir.
+if (OSKeyStoreTestUtils.canTestOSKeyStoreLogin() && OSKeyStore.canReauth()) {
+ // Enable OS reauth so we can test it.
+ sinon.stub(LoginHelper, "getOSAuthEnabled").returns(true);
+ registerCleanupFunction(() => {
+ sinon.restore();
+ });
+}
+
var { LoginTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/LoginTestUtils.sys.mjs"
);
@@ -53,6 +71,15 @@ let TEST_LOGIN3 = new nsLoginInfo(
);
TEST_LOGIN3.QueryInterface(Ci.nsILoginMetaInfo).timePasswordChanged = 123456;
+const PASSWORDS_OS_REAUTH_PREF = "signon.management.page.os-auth.optout";
+const CryptoErrors = {
+ USER_CANCELED_PASSWORD: "User canceled primary password entry",
+ ENCRYPTION_FAILURE: "Couldn't encrypt string",
+ INVALID_ARG_ENCRYPT: "Need at least one plaintext to encrypt",
+ INVALID_ARG_DECRYPT: "Need at least one ciphertext to decrypt",
+ DECRYPTION_FAILURE: "Couldn't decrypt string",
+};
+
async function addLogin(login) {
const result = await Services.logins.addLoginAsync(login);
registerCleanupFunction(() => {
@@ -131,7 +158,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")) {
@@ -153,6 +180,12 @@ add_setup(async function setup_head() {
// Ignore MarionetteEvents error (Bug 1730837, Bug 1710079).
return;
}
+ if (msg.errorMessage.includes(CryptoErrors.DECRYPTION_FAILURE)) {
+ // Ignore decyption errors, we want to test if decryption failed
+ // But we cannot use try / catch in the test to catch this for some reason
+ // Bug 1403081 and Bug 1877720
+ return;
+ }
Assert.ok(false, msg.message || msg.errorMessage);
});
diff --git a/browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html b/browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html
index 68a58aee4f..afbae0c310 100644
--- a/browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html
+++ b/browser/components/aboutlogins/tests/chrome/test_confirm_delete_dialog.html
@@ -24,6 +24,7 @@ Test the confirmation-dialog component
</pre>
<script>
/** Test the confirmation-dialog component **/
+let isWin = navigator.platform.includes("Win");
let options = {
title: "confirm-delete-dialog-title",
@@ -65,7 +66,7 @@ add_task(async function test_initial_focus() {
add_task(async function test_tab_focus() {
gConfirmationDialog.show(options);
ok(!gConfirmationDialog.hidden, "The dialog should be visible");
- sendKey("TAB");
+ synthesizeKey("VK_TAB", { shiftKey: !isWin });
is(gConfirmationDialog.shadowRoot.activeElement, cancelButton,
"After opening the dialog and tabbing once, the cancel button should be focused");
gConfirmationDialog.hide();
@@ -86,7 +87,7 @@ add_task(async function test_enter_key_to_cancel() {
add_task(async function test_enter_key_to_confirm() {
let showPromise = gConfirmationDialog.show(options);
ok(!gConfirmationDialog.hidden, "The dialog should be visible");
- sendKey("TAB");
+ synthesizeKey("VK_TAB", { shiftKey: !isWin });
sendKey("RETURN");
try {
await showPromise;
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..0bde196f29 100644
--- a/browser/components/aboutwelcome/content/aboutwelcome.css
+++ b/browser/components/aboutwelcome/content/aboutwelcome.css
@@ -434,7 +434,7 @@ div#feature-callout.hidden {
inset-inline: auto 0;
margin-block: 16px 0;
margin-inline: 0 16px;
- background-color: var(--fc-background);
+ background-color: transparent;
}
#feature-callout .screen[pos=callout] .section-main .dismiss-button[button-size=small] {
height: 24px;
@@ -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/content/aboutwelcome.html b/browser/components/aboutwelcome/content/aboutwelcome.html
index eb56c63110..e7b9801092 100644
--- a/browser/components/aboutwelcome/content/aboutwelcome.html
+++ b/browser/components/aboutwelcome/content/aboutwelcome.html
@@ -27,7 +27,6 @@
<link rel="localization" href="browser/newtab/onboarding.ftl" />
<link rel="localization" href="browser/spotlight.ftl" />
<link rel="localization" href="browser/migrationWizard.ftl" />
- <link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
</head>
<body>
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_multistage_mr.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_mr.js
index 1832b75778..8831947e2f 100644
--- a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_mr.js
+++ b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_multistage_mr.js
@@ -362,7 +362,7 @@ add_task(async function test_aboutwelcome_embedded_migration() {
await ContentTaskUtils.waitForEvent(selector, "focus");
}
- selector.click();
+ EventUtils.synthesizeMouseAtCenter(selector, {}, wizard.ownerGlobal);
await shown;
let panelRect = panelList.getBoundingClientRect();
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/styles/_feature-callout.scss b/browser/components/asrouter/content-src/styles/_feature-callout.scss
index 40137fd29a..8a1b96db6f 100644
--- a/browser/components/asrouter/content-src/styles/_feature-callout.scss
+++ b/browser/components/asrouter/content-src/styles/_feature-callout.scss
@@ -359,7 +359,7 @@
inset-inline: auto 0;
margin-block: 16px 0;
margin-inline: 0 16px;
- background-color: var(--fc-background);
+ background-color: transparent;
&[button-size='small'] {
height: 24px;
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..aa5989ca29 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,12 @@ for (const type of [
"UPDATE_PINNED_SEARCH_SHORTCUTS",
"UPDATE_SEARCH_SHORTCUTS",
"UPDATE_SECTION_PREFS",
+ "WALLPAPERS_SET",
+ "WALLPAPER_CLICK",
+ "WEATHER_IMPRESSION",
+ "WEATHER_LOAD_ERROR",
+ "WEATHER_OPEN_PROVIDER_URL",
+ "WEATHER_UPDATE",
"WEBEXT_CLICK",
"WEBEXT_DISMISS",
]) {
@@ -550,8 +553,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 +967,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 +992,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 +1444,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/content/components/ASRouterAdmin/ASRouterAdmin.css b/browser/components/asrouter/content/components/ASRouterAdmin/ASRouterAdmin.css
index de14572006..2e16438917 100644
--- a/browser/components/asrouter/content/components/ASRouterAdmin/ASRouterAdmin.css
+++ b/browser/components/asrouter/content/components/ASRouterAdmin/ASRouterAdmin.css
@@ -11,6 +11,16 @@
--newtab-text-secondary-color: color-mix(in srgb, var(--newtab-text-primary-color) 70%, transparent);
--newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #000);
--newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #000);
+ --newtab-button-background: var(--button-background-color);
+ --newtab-button-focus-background: var(--newtab-button-background);
+ --newtab-button-focus-border: var(--focus-outline-color);
+ --newtab-button-hover-background: var(--button-background-color-hover);
+ --newtab-button-active-background: var(--button-background-color-active);
+ --newtab-button-text: var(--button-text-color);
+ --newtab-button-static-background: #F0F0F4;
+ --newtab-button-static-focus-background: var(--newtab-button-static-background);
+ --newtab-button-static-hover-background: #E0E0E6;
+ --newtab-button-static-active-background: #CFCFD8;
--newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent);
--newtab-element-secondary-hover-color: color-mix(in srgb, currentColor 12%, transparent);
--newtab-element-secondary-active-color: color-mix(in srgb, currentColor 25%, transparent);
@@ -48,6 +58,9 @@
--newtab-primary-element-text-color: rgb(43, 42, 51);
--newtab-wordmark-color: rgb(251, 251, 254);
--newtab-status-success: #7C6;
+ --newtab-button-static-background: #2B2A33;
+ --newtab-button-static-hover-background: #52525E;
+ --newtab-button-static-active-background: #5B5B66;
}
@media (prefers-contrast) {
@@ -61,7 +74,7 @@
background-size: 16px;
-moz-context-properties: fill;
display: inline-block;
- color: var(--newtab-text-primary-color);
+ color: var(--icon-color);
fill: currentColor;
height: 16px;
vertical-align: middle;
@@ -111,6 +124,9 @@
.icon.icon-info {
background-image: url("chrome://global/skin/icons/info.svg");
}
+.icon.icon-info-critical {
+ background-image: url("chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg");
+}
.icon.icon-help {
background-image: url("chrome://global/skin/icons/help.svg");
}
@@ -191,6 +207,9 @@
.icon.icon-webextension {
background-image: url("chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg");
}
+.icon.icon-weather {
+ background-image: url("chrome://browser/skin/weather/sunny.svg");
+}
.icon.icon-highlights {
background-image: url("chrome://global/skin/icons/highlights.svg");
}
diff --git a/browser/components/asrouter/docs/targeting-attributes.md b/browser/components/asrouter/docs/targeting-attributes.md
index 89c5a6b6c6..cabcd661f8 100644
--- a/browser/components/asrouter/docs/targeting-attributes.md
+++ b/browser/components/asrouter/docs/targeting-attributes.md
@@ -34,7 +34,6 @@ Please note that some targeting attributes require stricter controls on the tele
* [hasMigratedPasswords](#hasmigratedpasswords)
* [hasPinnedTabs](#haspinnedtabs)
* [homePageSettings](#homepagesettings)
-* [inMr2022Holdback](#inmr2022holdback)
* [isBackgroundTaskMode](#isbackgroundtaskmode)
* [isChinaRepack](#ischinarepack)
* [isDefaultBrowser](#isdefaultbrowser)
@@ -43,8 +42,8 @@ Please note that some targeting attributes require stricter controls on the tele
* [isFxAEnabled](#isfxaenabled)
* [isFxASignedIn](#isFxASignedIn)
* [isMajorUpgrade](#ismajorupgrade)
+* [isMSIX](#ismsix)
* [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
@@ -983,10 +972,6 @@ mode, or `null` if this invocation is not running in background task mode.
Checks if user prefers reduced motion as indicated by the value of a media query for `prefers-reduced-motion`.
-### `inMr2022Holdback`
-
-A boolean. `true` when the user is in the Major Release 2022 holdback study.
-
### `distributionId`
A string containing the id of the distribution, or the empty string if there
@@ -1020,6 +1005,10 @@ A boolean. `true` if the user is configured to use the embedded Migration Wizard
A boolean. `true` when [RTAMO](first-run.md#return-to-amo-rtamo) has been used to download Firefox, `false` otherwise.
+### `isMSIX`
+
+A boolean. `true` when hasPackageId is `true` on Windows, `false` otherwise.
+
### `isDeviceMigration`
A boolean. `true` when [support.mozilla.org](https://support.mozilla.org) has been used to download the browser as part of a "migration" campaign, for device migration guidance, `false` otherwise.
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..2761481ceb 100644
--- a/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs
+++ b/browser/components/asrouter/modules/ASRouterTargeting.sys.mjs
@@ -45,7 +45,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs",
CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
HomePage: "resource:///modules/HomePage.sys.mjs",
- NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
Region: "resource://gre/modules/Region.sys.mjs",
TargetingContext: "resource://messaging-system/targeting/Targeting.sys.mjs",
@@ -74,12 +73,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
- "isWhatsNewPanelEnabled",
- "browser.messaging-system.whatsNewPanel.enabled",
- false
-);
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
"hasAccessedFxAPanel",
"identity.fxaccounts.toolbar.accessed",
false
@@ -704,9 +697,6 @@ const TargetingGetters = {
get hasAccessedFxAPanel() {
return lazy.hasAccessedFxAPanel;
},
- get isWhatsNewPanelEnabled() {
- return lazy.isWhatsNewPanelEnabled;
- },
get userPrefs() {
return {
cfrFeatures: lazy.cfrFeaturesUserPref,
@@ -856,6 +846,19 @@ const TargetingGetters = {
return lazy.WindowsLaunchOnLogin.getLaunchOnLoginEnabled();
},
+ get isMSIX() {
+ if (AppConstants.platform !== "win") {
+ return false;
+ }
+ // While we can write registry keys using external programs, we have no
+ // way of cleanup on uninstall. If we are on an MSIX build
+ // launch on login should never be enabled.
+ // Default to false so that the feature isn't unnecessarily
+ // disabled.
+ // See Bug 1888263.
+ return Services.sysinfo.getProperty("hasWinPackageId", false);
+ },
+
/**
* Is this invocation running in background task mode?
*
@@ -888,15 +891,6 @@ const TargetingGetters = {
},
/**
- * Whether or not the user is in the Major Release 2022 holdback study.
- */
- get inMr2022Holdback() {
- return (
- lazy.NimbusFeatures.majorRelease2022.getVariable("onboarding") === false
- );
- },
-
- /**
* The distribution id, if any.
* @return {string}
*/
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/CFRMessageProvider.sys.mjs b/browser/components/asrouter/modules/CFRMessageProvider.sys.mjs
index e0aa49ad49..c80ae323ab 100644
--- a/browser/components/asrouter/modules/CFRMessageProvider.sys.mjs
+++ b/browser/components/asrouter/modules/CFRMessageProvider.sys.mjs
@@ -811,6 +811,74 @@ const CFR_MESSAGES = [
},
trigger: { id: "preferenceObserver", params: ["foo.bar"] },
},
+ {
+ id: "FACEBOOK_CONTAINER_ADDON_A",
+ template: "cfr_doorhanger",
+ groups: ["cfr"],
+ content: {
+ layout: "addon_recommendation",
+ category: "cfrAddons",
+ bucket_id: "CFR",
+ anchor_id: "PanelUI-menu-button",
+ skip_address_bar_notifier: true,
+ icon_class: "cfr-doorhanger-medium-icon",
+ notification_text: {
+ string_id: "cfr-doorhanger-extension-notification2",
+ },
+ heading_text: {
+ string_id: "cfr-doorhanger-extension-heading",
+ },
+ info_icon: {
+ label: {
+ string_id: "cfr-doorhanger-extension-sumo-link",
+ },
+ sumo_path: "extensionrecommendations",
+ },
+ addon: {
+ id: "954390",
+ title: "Facebook Container",
+ icon: "https://firefox-settings-attachments.cdn.mozilla.net/main-workspace/ms-images/03c866df-82ea-489c-83c7-df6d0662d893.svg",
+ rating: "4.5",
+ users: "1.1M",
+ author: "Mozilla",
+ amo_url: "https://addons.mozilla.org/firefox/addon/facebook-container/",
+ },
+ text: "Make it harder for Facebook to track your browsing activity, including info from medical and financial sites.",
+ buttons: {
+ primary: {
+ label: {
+ string_id: "firefoxview-cfr-primarybutton",
+ },
+ action: {
+ type: "INSTALL_ADDON_FROM_URL",
+ data: {
+ url: "https://example.com",
+ telemetrySource: "amo",
+ },
+ },
+ },
+ secondary: [
+ {
+ label: {
+ string_id: "firefoxview-cfr-secondarybutton",
+ },
+ action: {
+ type: "CANCEL",
+ },
+ },
+ ],
+ },
+ },
+ frequency: {
+ lifetime: 1,
+ },
+ targeting:
+ "!('@contain-facebook' in addonsInfo.addons|keys) && !('@testpilot-containers' in addonsInfo.addons|keys) && ('browser.discovery.enabled'|preferenceValue)",
+ trigger: {
+ id: "openURL",
+ params: ["www.facebook.com", "facebook.com"],
+ },
+ },
];
export const CFRMessageProvider = {
diff --git a/browser/components/asrouter/modules/FeatureCallout.sys.mjs b/browser/components/asrouter/modules/FeatureCallout.sys.mjs
index 5f0e266a4e..e8732b213d 100644
--- a/browser/components/asrouter/modules/FeatureCallout.sys.mjs
+++ b/browser/components/asrouter/modules/FeatureCallout.sys.mjs
@@ -462,6 +462,10 @@ export class FeatureCallout {
* the callout should be aligned with which point on the anchor element.
* @property {PopupAttachmentPoint} anchor_attachment
* @property {PopupAttachmentPoint} callout_attachment
+ * @property {String} [panel_position_string] The attachments joined into a
+ * string, e.g. "bottomleft topright". Passed to XULPopupElement::openPopup.
+ * This is not provided by JSON, but generated from anchor_attachment and
+ * callout_attachment.
* @property {Number} [offset_x] Offset in pixels to apply to the callout
* position in the horizontal direction.
* @property {Number} [offset_y] The same in the vertical direction.
@@ -514,8 +518,10 @@ export class FeatureCallout {
*/
/**
- * @typedef {Object} AnchorConfig
+ * @typedef {Object} Anchor
* @property {String} selector CSS selector for the anchor node.
+ * @property {Element} [element] The anchor node resolved from the selector.
+ * Not provided by JSON, but generated dynamically.
* @property {PanelPosition} [panel_position] Used to show the callout in a
* XUL panel. Only works in chrome documents, like the main browser window.
* @property {HTMLArrowPosition} [arrow_position] Used to show the callout in
@@ -533,27 +539,13 @@ export class FeatureCallout {
*/
/**
- * @typedef {Object} Anchor
- * @property {String} selector
- * @property {PanelPosition} [panel_position]
- * @property {HTMLArrowPosition} [arrow_position]
- * @property {PositionOverride} [absolute_position]
- * @property {Boolean} [hide_arrow]
- * @property {Boolean} [no_open_on_anchor]
- * @property {Number} [arrow_width]
- * @property {Element} element The anchor node resolved from the selector.
- * @property {String} [panel_position_string] The panel_position joined into a
- * string, e.g. "bottomleft topright". Passed to XULPopupElement::openPopup.
- */
-
- /**
* Return the first visible anchor element for the current screen. Screens can
* specify multiple anchors in an array, and the first one that is visible
* will be used. If none are visible, return null.
* @returns {Anchor|null}
*/
_getAnchor() {
- /** @type {AnchorConfig[]} */
+ /** @type {Anchor[]} */
const anchors = Array.isArray(this.currentScreen?.anchors)
? this.currentScreen.anchors
: [];
@@ -565,9 +557,9 @@ export class FeatureCallout {
continue;
}
const { selector, arrow_position, panel_position } = anchor;
- let panel_position_string;
if (panel_position) {
- panel_position_string = this._getPanelPositionString(panel_position);
+ let panel_position_string =
+ this._getPanelPositionString(panel_position);
// if the positionString doesn't match the format we expect, don't
// render the callout.
if (!panel_position_string && !arrow_position) {
@@ -580,6 +572,7 @@ export class FeatureCallout {
);
continue;
}
+ panel_position.panel_position_string = panel_position_string;
}
if (
arrow_position &&
@@ -637,7 +630,7 @@ export class FeatureCallout {
continue;
}
}
- return { ...anchor, panel_position_string, element };
+ return { ...anchor, element };
}
return null;
}
@@ -752,13 +745,10 @@ export class FeatureCallout {
}
const { autohide, padding } = this.currentScreen.content;
- const {
- panel_position_string,
- hide_arrow,
- no_open_on_anchor,
- arrow_width,
- } = anchor;
- const needsPanel = "MozXULElement" in this.win && !!panel_position_string;
+ const { panel_position, hide_arrow, no_open_on_anchor, arrow_width } =
+ anchor;
+ const needsPanel =
+ "MozXULElement" in this.win && !!panel_position?.panel_position_string;
if (this._container) {
if (needsPanel ^ (this._container?.localName === "panel")) {
@@ -775,7 +765,7 @@ export class FeatureCallout {
noautofocus="true"
flip="slide"
type="arrow"
- position="${panel_position_string}"
+ position="${panel_position.panel_position_string}"
${hide_arrow ? "" : 'show-arrow=""'}
${autohide ? "" : 'noautohide="true"'}
${no_open_on_anchor ? 'no-open-on-anchor=""' : ""}
@@ -1742,17 +1732,21 @@ export class FeatureCallout {
});
} else if (this._container.localName === "panel") {
const anchor = this._getAnchor();
- if (!anchor) {
+ if (!anchor?.panel_position) {
this.endTour();
return;
}
- const position = anchor.panel_position_string;
+ const {
+ panel_position_string: position,
+ offset_x: x,
+ offset_y: y,
+ } = anchor.panel_position;
this._container.addEventListener("popupshown", onRender, {
once: true,
});
this._container.addEventListener("popuphiding", this);
this._addPanelConflictListeners();
- this._container.openPopup(anchor.element, { position });
+ this._container.openPopup(anchor.element, { position, x, y });
}
}
});
diff --git a/browser/components/asrouter/modules/FeatureCalloutMessages.sys.mjs b/browser/components/asrouter/modules/FeatureCalloutMessages.sys.mjs
index 38c9a8d848..4fda4355bf 100644
--- a/browser/components/asrouter/modules/FeatureCalloutMessages.sys.mjs
+++ b/browser/components/asrouter/modules/FeatureCalloutMessages.sys.mjs
@@ -157,7 +157,7 @@ const MESSAGES = () => {
// Add the highest possible cap to ensure impressions are recorded while allowing the Spotlight to sync across windows/tabs with Firefox View open
lifetime: 100,
},
- targeting: `!inMr2022Holdback && source == "about:firefoxview" &&
+ targeting: `source == "about:firefoxview" &&
!'browser.newtabpage.activity-stream.asrouter.providers.cfr'|preferenceIsUserSet &&
'browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features'|preferenceValue &&
${matchCurrentScreenTargeting(
@@ -303,7 +303,7 @@ const MESSAGES = () => {
],
},
priority: 3,
- targeting: `!inMr2022Holdback && source == "about:firefoxview" && ${matchCurrentScreenTargeting(
+ targeting: `source == "about:firefoxview" && ${matchCurrentScreenTargeting(
FIREFOX_VIEW_PREF,
"FEATURE_CALLOUT_[0-9]"
)} && ${matchIncompleteTargeting(FIREFOX_VIEW_PREF)}`,
@@ -376,7 +376,7 @@ const MESSAGES = () => {
],
},
priority: 2,
- targeting: `!inMr2022Holdback && source == "about:firefoxview" && "browser.firefox-view.view-count" | preferenceValue > 2
+ targeting: `source == "about:firefoxview" && "browser.firefox-view.view-count" | preferenceValue > 2
&& (("identity.fxaccounts.enabled" | preferenceValue == false) || !(("services.sync.engine.tabs" | preferenceValue == true) && ("services.sync.username" | preferenceValue))) && (!messageImpressions.FIREFOX_VIEW_SPOTLIGHT[messageImpressions.FIREFOX_VIEW_SPOTLIGHT | length - 1] || messageImpressions.FIREFOX_VIEW_SPOTLIGHT[messageImpressions.FIREFOX_VIEW_SPOTLIGHT | length - 1] < currentDate|date - ${ONE_DAY_IN_MS})`,
frequency: {
lifetime: 1,
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..298599e42b 100644
--- a/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs
+++ b/browser/components/asrouter/modules/OnboardingMessageProvider.sys.mjs
@@ -24,7 +24,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
- NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
ShellService: "resource:///modules/ShellService.sys.mjs",
});
@@ -52,7 +51,6 @@ const L10N = new Localization([
"branding/brand.ftl",
"browser/newtab/onboarding.ftl",
"toolkit/branding/brandings.ftl",
- "toolkit/branding/accounts.ftl",
]);
const HOMEPAGE_PREF = "browser.startup.homepage";
@@ -872,7 +870,7 @@ const BASE_MESSAGES = () => [
],
lifetime: 12,
},
- targeting: "!inMr2022Holdback && doesAppNeedPrivatePin",
+ targeting: "doesAppNeedPrivatePin",
},
{
id: "PB_NEWTAB_COOKIE_BANNERS_PROMO",
@@ -988,7 +986,7 @@ const BASE_MESSAGES = () => [
targeting: `source == 'newtab'
&& 'browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt'|preferenceValue == false
&& 'browser.startup.windowsLaunchOnLogin.enabled'|preferenceValue == true && isDefaultBrowser && !activeNotifications
- && !launchOnLoginEnabled`,
+ && !launchOnLoginEnabled && !isMSIX`,
},
{
id: "INFOBAR_LAUNCH_ON_LOGIN_FINAL",
@@ -1056,7 +1054,7 @@ const BASE_MESSAGES = () => [
&& messageImpressions.INFOBAR_LAUNCH_ON_LOGIN[messageImpressions.INFOBAR_LAUNCH_ON_LOGIN | length - 1]
&& messageImpressions.INFOBAR_LAUNCH_ON_LOGIN[messageImpressions.INFOBAR_LAUNCH_ON_LOGIN | length - 1] <
currentDate|date - ${FOURTEEN_DAYS_IN_MS}
- && !launchOnLoginEnabled`,
+ && !launchOnLoginEnabled && !isMSIX`,
},
{
id: "FOX_DOODLE_SET_DEFAULT",
@@ -1210,6 +1208,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
@@ -1275,9 +1373,8 @@ export const OnboardingMessageProvider = {
return checkDefault && !isDefault;
},
_shouldShowPrivacySegmentationScreen() {
- // Fall back to pref: browser.privacySegmentation.preferences.show
- return lazy.NimbusFeatures.majorRelease2022.getVariable(
- "feltPrivacyShowPreferencesSection"
+ return Services.prefs.getBoolPref(
+ "browser.privacySegmentation.preferences.show"
);
},
_doesHomepageNeedReset() {
diff --git a/browser/components/asrouter/modules/PanelTestProvider.sys.mjs b/browser/components/asrouter/modules/PanelTestProvider.sys.mjs
index 7a7ff1e1fc..8dbb718bfd 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"],
@@ -165,7 +37,7 @@ const MESSAGES = () => [
},
sumo_path: "https://example.com",
},
- text: { string_id: "cfr-doorhanger-bookmark-fxa-body" },
+ text: { string_id: "cfr-doorhanger-bookmark-fxa-body-2" },
icon: "chrome://branding/content/icon64.png",
icon_class: "cfr-doorhanger-large-icon",
persistent_doorhanger: true,
diff --git a/browser/components/asrouter/modules/RemoteL10n.sys.mjs b/browser/components/asrouter/modules/RemoteL10n.sys.mjs
index 1df10fbd72..4135d77191 100644
--- a/browser/components/asrouter/modules/RemoteL10n.sys.mjs
+++ b/browser/components/asrouter/modules/RemoteL10n.sys.mjs
@@ -204,7 +204,6 @@ export class _RemoteL10n {
"branding/brand.ftl",
"browser/defaultBrowserNotification.ftl",
"browser/newtab/asrouter.ftl",
- "toolkit/branding/accounts.ftl",
"toolkit/branding/brandings.ftl",
],
false
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_infobar.js b/browser/components/asrouter/tests/browser/browser_asrouter_infobar.js
index b80b3ec7a4..a01b2cf14c 100644
--- a/browser/components/asrouter/tests/browser/browser_asrouter_infobar.js
+++ b/browser/components/asrouter/tests/browser/browser_asrouter_infobar.js
@@ -107,7 +107,7 @@ add_task(async function react_to_trigger() {
"Notification has default priority"
);
// Dismiss the notification
- notificationStack.currentNotification.closeButtonEl.click();
+ notificationStack.currentNotification.closeButton.click();
});
add_task(async function dismiss_telemetry() {
@@ -128,7 +128,7 @@ add_task(async function dismiss_telemetry() {
// Remove any IMPRESSION pings
dispatchStub.reset();
- infobar.notification.closeButtonEl.click();
+ infobar.notification.closeButton.click();
await BrowserTestUtils.waitForCondition(
() => infobar.notification === null,
@@ -204,7 +204,7 @@ add_task(async function prevent_multiple_messages() {
Assert.equal(dispatchStub.callCount, 2, "Impression count did not increase");
// Dismiss the first notification
- infobar.notification.closeButtonEl.click();
+ infobar.notification.closeButton.click();
Assert.equal(InfoBar._activeInfobar, null, "Cleared the active notification");
// Reset impressions count
@@ -218,6 +218,6 @@ add_task(async function prevent_multiple_messages() {
Assert.ok(InfoBar._activeInfobar, "activeInfobar is set");
Assert.equal(dispatchStub.callCount, 2, "Called twice with IMPRESSION");
// Dismiss the notification again
- infobar.notification.closeButtonEl.click();
+ infobar.notification.closeButton.click();
Assert.equal(InfoBar._activeInfobar, null, "Cleared the active notification");
});
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/browser/browser_asrouter_targeting.js b/browser/components/asrouter/tests/browser/browser_asrouter_targeting.js
index 55f278fd7d..e20a75c5ab 100644
--- a/browser/components/asrouter/tests/browser/browser_asrouter_targeting.js
+++ b/browser/components/asrouter/tests/browser/browser_asrouter_targeting.js
@@ -1226,47 +1226,6 @@ add_task(async function check_userPrefersReducedMotion() {
);
});
-add_task(async function test_mr2022Holdback() {
- await ExperimentAPI.ready();
-
- ok(
- !ASRouterTargeting.Environment.inMr2022Holdback,
- "Should not be in holdback (no experiment)"
- );
-
- {
- const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
- featureId: "majorRelease2022",
- value: {
- onboarding: true,
- },
- });
-
- ok(
- !ASRouterTargeting.Environment.inMr2022Holdback,
- "Should not be in holdback (onboarding = true)"
- );
-
- await doExperimentCleanup();
- }
-
- {
- const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
- featureId: "majorRelease2022",
- value: {
- onboarding: false,
- },
- });
-
- ok(
- ASRouterTargeting.Environment.inMr2022Holdback,
- "Should be in holdback (onboarding = false)"
- );
-
- await doExperimentCleanup();
- }
-});
-
add_task(async function test_distributionId() {
is(
ASRouterTargeting.Environment.distributionId,
@@ -1469,6 +1428,28 @@ add_task(async function check_useEmbeddedMigrationWizard() {
ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard));
});
+add_task(async function check_isMSIX() {
+ is(
+ typeof ASRouterTargeting.Environment.isMSIX,
+ "boolean",
+ "Should return a boolean"
+ );
+ if (AppConstants.platform !== "win") {
+ is(
+ ASRouterTargeting.Environment.isMSIX,
+ false,
+ "Should always be false on non-Windows"
+ );
+ return;
+ }
+
+ is(
+ ASRouterTargeting.Environment.isMSIX,
+ Services.sysinfo.getProperty("hasWinPackageId"),
+ "Should match the value from sysinfo"
+ );
+});
+
add_task(async function check_isRTAMO() {
is(
typeof ASRouterTargeting.Environment.isRTAMO,
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/CFRMessageProvider.test.js b/browser/components/asrouter/tests/unit/CFRMessageProvider.test.js
index fe6959852c..ec35806d11 100644
--- a/browser/components/asrouter/tests/unit/CFRMessageProvider.test.js
+++ b/browser/components/asrouter/tests/unit/CFRMessageProvider.test.js
@@ -1,32 +1,9 @@
import { CFRMessageProvider } from "modules/CFRMessageProvider.sys.mjs";
-const REGULAR_IDS = [
- "FACEBOOK_CONTAINER",
- "GOOGLE_TRANSLATE",
- "YOUTUBE_ENHANCE",
- // These are excluded for now.
- // "WIKIPEDIA_CONTEXT_MENU_SEARCH",
- // "REDDIT_ENHANCEMENT",
-];
-
describe("CFRMessageProvider", () => {
let messages;
beforeEach(async () => {
messages = await CFRMessageProvider.getMessages();
});
- it("should have a total of 11 messages", () => {
- assert.lengthOf(messages, 11);
- });
- it("should have one message each for the three regular addons", () => {
- for (const id of REGULAR_IDS) {
- const cohort3 = messages.find(msg => msg.id === `${id}_3`);
- assert.ok(cohort3, `contains three day cohort for ${id}`);
- assert.deepEqual(
- cohort3.frequency,
- { lifetime: 3 },
- "three day cohort has the right frequency cap"
- );
- assert.notInclude(cohort3.targeting, `providerCohorts.cfr`);
- }
- });
+ it("should have messages", () => assert.ok(messages.length));
});
diff --git a/browser/components/asrouter/tests/unit/RemoteL10n.test.js b/browser/components/asrouter/tests/unit/RemoteL10n.test.js
index dd0f858750..0e32442522 100644
--- a/browser/components/asrouter/tests/unit/RemoteL10n.test.js
+++ b/browser/components/asrouter/tests/unit/RemoteL10n.test.js
@@ -81,7 +81,6 @@ describe("RemoteL10n", () => {
"branding/brand.ftl",
"browser/defaultBrowserNotification.ftl",
"browser/newtab/asrouter.ftl",
- "toolkit/branding/accounts.ftl",
"toolkit/branding/brandings.ftl",
]);
assert.isFalse(args[1]);
@@ -103,7 +102,6 @@ describe("RemoteL10n", () => {
"branding/brand.ftl",
"browser/defaultBrowserNotification.ftl",
"browser/newtab/asrouter.ftl",
- "toolkit/branding/accounts.ftl",
"toolkit/branding/brandings.ftl",
]);
assert.isFalse(args[1]);
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..05634ed2c8 100644
--- a/browser/components/backup/BackupService.sys.mjs
+++ b/browser/components/backup/BackupService.sys.mjs
@@ -2,7 +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/. */
-import * as BackupResources from "resource:///modules/backup/BackupResources.sys.mjs";
+import * as DefaultBackupResources from "resource:///modules/backup/BackupResources.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
@@ -15,12 +16,25 @@ ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
});
});
+ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
+ return ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ ).getFxAccountsSingleton();
+});
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ClientID: "resource://gre/modules/ClientID.sys.mjs",
+ JsonSchemaValidator:
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
+ UIState: "resource://services-sync/UIState.sys.mjs",
+});
+
/**
* The BackupService class orchestrates the scheduling and creation of profile
* backups. It also does most of the heavy lifting for the restoration of a
* profile backup.
*/
-export class BackupService {
+export class BackupService extends EventTarget {
/**
* The BackupService singleton instance.
*
@@ -37,6 +51,140 @@ export class BackupService {
#resources = new Map();
/**
+ * Set to true if a backup is currently in progress. Causes stateUpdate()
+ * to be called.
+ *
+ * @see BackupService.stateUpdate()
+ * @param {boolean} val
+ * True if a backup is in progress.
+ */
+ set #backupInProgress(val) {
+ if (this.#_state.backupInProgress != val) {
+ this.#_state.backupInProgress = val;
+ this.stateUpdate();
+ }
+ }
+
+ /**
+ * True if a backup is currently in progress.
+ *
+ * @type {boolean}
+ */
+ get #backupInProgress() {
+ return this.#_state.backupInProgress;
+ }
+
+ /**
+ * Dispatches an event to let listeners know that the BackupService state
+ * object has been updated.
+ */
+ stateUpdate() {
+ this.dispatchEvent(new CustomEvent("BackupService:StateUpdate"));
+ }
+
+ /**
+ * An object holding the current state of the BackupService instance, for
+ * the purposes of representing it in the user interface. Ideally, this would
+ * be named #state instead of #_state, but sphinx-js seems to be fairly
+ * unhappy with that coupled with the ``state`` getter.
+ *
+ * @type {object}
+ */
+ #_state = { backupInProgress: false };
+
+ /**
+ * A Promise that will resolve once the postRecovery steps are done. It will
+ * also resolve if postRecovery steps didn't need to run.
+ *
+ * @see BackupService.checkForPostRecovery()
+ * @type {Promise<undefined>}
+ */
+ #postRecoveryPromise;
+
+ /**
+ * The resolving function for #postRecoveryPromise, which should be called
+ * by checkForPostRecovery() before exiting.
+ *
+ * @type {Function}
+ */
+ #postRecoveryResolver;
+
+ /**
+ * The name of the backup manifest file.
+ *
+ * @type {string}
+ */
+ static get MANIFEST_FILE_NAME() {
+ return "backup-manifest.json";
+ }
+
+ /**
+ * The current schema version of the backup manifest that this BackupService
+ * uses when creating a backup.
+ *
+ * @type {number}
+ */
+ static get MANIFEST_SCHEMA_VERSION() {
+ return 1;
+ }
+
+ /**
+ * A promise that resolves to the schema for the backup manifest that this
+ * BackupService uses when creating a backup. This should be accessed via
+ * the `MANIFEST_SCHEMA` static getter.
+ *
+ * @type {Promise<object>}
+ */
+ static #manifestSchemaPromise = null;
+
+ /**
+ * The current schema version of the backup manifest that this BackupService
+ * uses when creating a backup.
+ *
+ * @type {Promise<object>}
+ */
+ static get MANIFEST_SCHEMA() {
+ if (!BackupService.#manifestSchemaPromise) {
+ BackupService.#manifestSchemaPromise = BackupService._getSchemaForVersion(
+ BackupService.MANIFEST_SCHEMA_VERSION
+ );
+ }
+
+ return BackupService.#manifestSchemaPromise;
+ }
+
+ /**
+ * The name of the post recovery file written into the newly created profile
+ * directory just after a profile is recovered from a backup.
+ *
+ * @type {string}
+ */
+ static get POST_RECOVERY_FILE_NAME() {
+ return "post-recovery.json";
+ }
+
+ /**
+ * Returns the schema for the backup manifest for a given version.
+ *
+ * This should really be #getSchemaForVersion, but for some reason,
+ * sphinx-js seems to choke on static async private methods (bug 1893362).
+ * We workaround this breakage by using the `_` prefix to indicate that this
+ * method should be _considered_ private, and ask that you not use this method
+ * outside of this class. The sphinx-js issue is tracked at
+ * https://github.com/mozilla/sphinx-js/issues/240.
+ *
+ * @private
+ * @param {number} version
+ * The version of the schema to return.
+ * @returns {Promise<object>}
+ */
+ static async _getSchemaForVersion(version) {
+ let schemaURL = `chrome://browser/content/backup/BackupManifest.${version}.schema.json`;
+ let response = await fetch(schemaURL);
+ return response.json();
+ }
+
+ /**
* 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,24 +196,549 @@ export class BackupService {
if (this.#instance) {
return this.#instance;
}
- this.#instance = new BackupService(BackupResources);
- this.#instance.takeMeasurements();
+ this.#instance = new BackupService(DefaultBackupResources);
+
+ this.#instance.checkForPostRecovery().then(() => {
+ 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) {
+ super();
lazy.logConsole.debug("Instantiated");
for (const resourceName in backupResources) {
- let resource = BackupResources[resourceName];
+ let resource = backupResources[resourceName];
this.#resources.set(resource.key, resource);
}
+
+ let { promise, resolve } = Promise.withResolvers();
+ this.#postRecoveryPromise = promise;
+ this.#postRecoveryResolver = resolve;
+ }
+
+ /**
+ * Returns a reference to a Promise that will resolve with undefined once
+ * postRecovery steps have had a chance to run. This will also be resolved
+ * with undefined if no postRecovery steps needed to be run.
+ *
+ * @see BackupService.checkForPostRecovery()
+ * @returns {Promise<undefined>}
+ */
+ get postRecoveryComplete() {
+ return this.#postRecoveryPromise;
+ }
+
+ /**
+ * Returns a state object describing the state of the BackupService for the
+ * purposes of representing it in the user interface. The returned state
+ * object is immutable.
+ *
+ * @type {object}
+ */
+ get state() {
+ return Object.freeze(structuredClone(this.#_state));
+ }
+
+ /**
+ * @typedef {object} CreateBackupResult
+ * @property {string} stagingPath
+ * The staging path for where the backup was created.
+ */
+
+ /**
+ * 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<CreateBackupResult|null>}
+ * A promise that resolves to an object containing the path to the staging
+ * folder where the backup was created, or null if the backup failed.
+ */
+ 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 null;
+ }
+
+ this.#backupInProgress = true;
+
+ try {
+ lazy.logConsole.debug(`Creating backup for profile at ${profilePath}`);
+ let manifest = await this.#createBackupManifest();
+
+ // 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);
+
+ // Sort resources be priority.
+ let sortedResources = Array.from(this.#resources.values()).sort(
+ (a, b) => {
+ return b.priority - a.priority;
+ }
+ );
+
+ // Perform the backup for each resource.
+ for (let resourceClass of sortedResources) {
+ 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
+ );
+
+ if (manifestEntry === undefined) {
+ lazy.logConsole.error(
+ `Backup of resource with key ${resourceClass.key} returned undefined
+ as its ManifestEntry instead of null or an object`
+ );
+ } else {
+ lazy.logConsole.debug(
+ `Backup of resource with key ${resourceClass.key} completed`,
+ manifestEntry
+ );
+ manifest.resources[resourceClass.key] = manifestEntry;
+ }
+ } catch (e) {
+ lazy.logConsole.error(
+ `Failed to backup resource: ${resourceClass.key}`,
+ e
+ );
+ }
+ }
+
+ // Ensure that the manifest abides by the current schema, and log
+ // an error if somehow it doesn't. We'll want to collect telemetry for
+ // this case to make sure it's not happening in the wild. We debated
+ // throwing an exception here too, but that's not meaningfully better
+ // than creating a backup that's not schema-compliant. At least in this
+ // case, a user so-inclined could theoretically repair the manifest
+ // to make it valid.
+ let manifestSchema = await BackupService.MANIFEST_SCHEMA;
+ let schemaValidationResult = lazy.JsonSchemaValidator.validate(
+ manifest,
+ manifestSchema
+ );
+ if (!schemaValidationResult.valid) {
+ lazy.logConsole.error(
+ "Backup manifest does not conform to schema:",
+ manifest,
+ manifestSchema,
+ schemaValidationResult
+ );
+ // TODO: Collect telemetry for this case. (bug 1891817)
+ }
+
+ // Write the manifest to the staging folder.
+ let manifestPath = PathUtils.join(
+ stagingPath,
+ BackupService.MANIFEST_FILE_NAME
+ );
+ await IOUtils.writeJSON(manifestPath, manifest);
+
+ let renamedStagingPath = await this.#finalizeStagingFolder(stagingPath);
+ lazy.logConsole.log(
+ "Wrote backup to staging directory at ",
+ renamedStagingPath
+ );
+ return { stagingPath: renamedStagingPath };
+ } 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;
+ }
+
+ /**
+ * Renames the staging folder to an ISO 8601 date string with dashes replacing colons and fractional seconds stripped off.
+ * The ISO date string should be formatted from YYYY-MM-DDTHH:mm:ss.sssZ to YYYY-MM-DDTHH-mm-ssZ
+ *
+ * @param {string} stagingPath
+ * The path to the populated staging folder.
+ * @returns {Promise<string|null>}
+ * The path to the renamed staging folder, or null if the stagingPath was
+ * not pointing to a valid folder.
+ */
+ async #finalizeStagingFolder(stagingPath) {
+ if (!(await IOUtils.exists(stagingPath))) {
+ // If we somehow can't find the specified staging folder, cancel this step.
+ lazy.logConsole.error(
+ `Failed to finalize staging folder. Cannot find ${stagingPath}.`
+ );
+ return null;
+ }
+
+ try {
+ lazy.logConsole.debug("Finalizing and renaming staging folder");
+ let currentDateISO = new Date().toISOString();
+ // First strip the fractional seconds
+ let dateISOStripped = currentDateISO.replace(/\.\d+\Z$/, "Z");
+ // Now replace all colons with dashes
+ let dateISOFormatted = dateISOStripped.replaceAll(":", "-");
+
+ let stagingPathParent = PathUtils.parent(stagingPath);
+ let renamedBackupPath = PathUtils.join(
+ stagingPathParent,
+ dateISOFormatted
+ );
+ await IOUtils.move(stagingPath, renamedBackupPath);
+
+ let existingBackups = await IOUtils.getChildren(stagingPathParent);
+
+ /**
+ * Bug 1892532: for now, we only support a single backup file.
+ * If there are other pre-existing backup folders, delete them.
+ */
+ for (let existingBackupPath of existingBackups) {
+ if (existingBackupPath !== renamedBackupPath) {
+ await IOUtils.remove(existingBackupPath, {
+ recursive: true,
+ });
+ }
+ }
+ return renamedBackupPath;
+ } catch (e) {
+ lazy.logConsole.error(
+ `Something went wrong while finalizing the staging folder. ${e}`
+ );
+ throw e;
+ }
+ }
+
+ /**
+ * Creates and resolves with a backup manifest object with an empty resources
+ * property.
+ *
+ * @returns {Promise<object>}
+ */
+ async #createBackupManifest() {
+ let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+ let profileName;
+ if (!profileSvc.currentProfile) {
+ // We're probably running on a local build or in some special configuration.
+ // Let's pull in a profile name from the profile directory.
+ let profileFolder = PathUtils.split(PathUtils.profileDir).at(-1);
+ profileName = profileFolder.substring(profileFolder.indexOf(".") + 1);
+ } else {
+ profileName = profileSvc.currentProfile.name;
+ }
+
+ let meta = {
+ date: new Date().toISOString(),
+ appName: AppConstants.MOZ_APP_NAME,
+ appVersion: AppConstants.MOZ_APP_VERSION,
+ buildID: AppConstants.MOZ_BUILDID,
+ profileName,
+ machineName: lazy.fxAccounts.device.getLocalName(),
+ osName: Services.sysinfo.getProperty("name"),
+ osVersion: Services.sysinfo.getProperty("version"),
+ legacyClientID: await lazy.ClientID.getClientID(),
+ };
+
+ let fxaState = lazy.UIState.get();
+ if (fxaState.status == lazy.UIState.STATUS_SIGNED_IN) {
+ meta.accountID = fxaState.uid;
+ meta.accountEmail = fxaState.email;
+ }
+
+ return {
+ version: BackupService.MANIFEST_SCHEMA_VERSION,
+ meta,
+ resources: {},
+ };
+ }
+
+ /**
+ * Given a decompressed backup archive at recoveryPath, this method does the
+ * following:
+ *
+ * 1. Reads in the backup manifest from the archive and ensures that it is
+ * valid.
+ * 2. Creates a new named profile directory using the same name as the one
+ * found in the backup manifest, but with a different prefix.
+ * 3. Iterates over each resource in the manifest and calls the recover()
+ * method on each found BackupResource, passing in the associated
+ * ManifestEntry from the backup manifest, and collects any post-recovery
+ * data from those resources.
+ * 4. Writes a `post-recovery.json` file into the newly created profile
+ * directory.
+ * 5. Returns the name of the newly created profile directory.
+ *
+ * @param {string} recoveryPath
+ * The path to the decompressed backup archive on the file system.
+ * @param {boolean} [shouldLaunch=false]
+ * An optional argument that specifies whether an instance of the app
+ * should be launched with the newly recovered profile after recovery is
+ * complete.
+ * @param {string} [profileRootPath=null]
+ * An optional argument that specifies the root directory where the new
+ * profile directory should be created. If not provided, the default
+ * profile root directory will be used. This is primarily meant for
+ * testing.
+ * @returns {Promise<nsIToolkitProfile>}
+ * The nsIToolkitProfile that was created for the recovered profile.
+ * @throws {Exception}
+ * In the event that recovery somehow failed.
+ */
+ async recoverFromBackup(
+ recoveryPath,
+ shouldLaunch = false,
+ profileRootPath = null
+ ) {
+ lazy.logConsole.debug("Recovering from backup at ", recoveryPath);
+
+ try {
+ // Read in the backup manifest.
+ let manifestPath = PathUtils.join(
+ recoveryPath,
+ BackupService.MANIFEST_FILE_NAME
+ );
+ let manifest = await IOUtils.readJSON(manifestPath);
+ if (!manifest.version) {
+ throw new Error("Backup manifest version not found");
+ }
+
+ if (manifest.version > BackupService.MANIFEST_SCHEMA_VERSION) {
+ throw new Error(
+ "Cannot recover from a manifest newer than the current schema version"
+ );
+ }
+
+ // Make sure that it conforms to the schema.
+ let manifestSchema = await BackupService._getSchemaForVersion(
+ manifest.version
+ );
+ let schemaValidationResult = lazy.JsonSchemaValidator.validate(
+ manifest,
+ manifestSchema
+ );
+ if (!schemaValidationResult.valid) {
+ lazy.logConsole.error(
+ "Backup manifest does not conform to schema:",
+ manifest,
+ manifestSchema,
+ schemaValidationResult
+ );
+ // TODO: Collect telemetry for this case. (bug 1891817)
+ throw new Error("Cannot recover from an invalid backup manifest");
+ }
+
+ // In the future, if we ever bump the MANIFEST_SCHEMA_VERSION and need to
+ // do any special behaviours to interpret older schemas, this is where we
+ // can do that, and we can remove this comment.
+
+ let meta = manifest.meta;
+
+ // Okay, we have a valid backup-manifest.json. Let's create a new profile
+ // and start invoking the recover() method on each BackupResource.
+ let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+ let profile = profileSvc.createUniqueProfile(
+ profileRootPath ? await IOUtils.getDirectory(profileRootPath) : null,
+ meta.profileName
+ );
+
+ let postRecovery = {};
+
+ // Iterate over each resource in the manifest and call recover() on each
+ // associated BackupResource.
+ for (let resourceKey in manifest.resources) {
+ let manifestEntry = manifest.resources[resourceKey];
+ let resourceClass = this.#resources.get(resourceKey);
+ if (!resourceClass) {
+ lazy.logConsole.error(
+ `No BackupResource found for key ${resourceKey}`
+ );
+ continue;
+ }
+
+ try {
+ lazy.logConsole.debug(
+ `Restoring resource with key ${resourceKey}. ` +
+ `Requires encryption: ${resourceClass.requiresEncryption}`
+ );
+ let resourcePath = PathUtils.join(recoveryPath, resourceKey);
+ let postRecoveryEntry = await new resourceClass().recover(
+ manifestEntry,
+ resourcePath,
+ profile.rootDir.path
+ );
+ postRecovery[resourceKey] = postRecoveryEntry;
+ } catch (e) {
+ lazy.logConsole.error(
+ `Failed to recover resource: ${resourceKey}`,
+ e
+ );
+ }
+ }
+
+ // Make sure that a legacy telemetry client ID exists and is written to
+ // disk.
+ let clientID = await lazy.ClientID.getClientID();
+ lazy.logConsole.debug("Current client ID: ", clientID);
+ // Next, copy over the legacy telemetry client ID state from the currently
+ // running profile. The newly created profile that we're recovering into
+ // should inherit this client ID.
+ const TELEMETRY_STATE_FILENAME = "state.json";
+ const TELEMETRY_STATE_FOLDER = "datareporting";
+ await IOUtils.makeDirectory(
+ PathUtils.join(profile.rootDir.path, TELEMETRY_STATE_FOLDER)
+ );
+ await IOUtils.copy(
+ /* source */
+ PathUtils.join(
+ PathUtils.profileDir,
+ TELEMETRY_STATE_FOLDER,
+ TELEMETRY_STATE_FILENAME
+ ),
+ /* destination */
+ PathUtils.join(
+ profile.rootDir.path,
+ TELEMETRY_STATE_FOLDER,
+ TELEMETRY_STATE_FILENAME
+ )
+ );
+
+ let postRecoveryPath = PathUtils.join(
+ profile.rootDir.path,
+ BackupService.POST_RECOVERY_FILE_NAME
+ );
+ await IOUtils.writeJSON(postRecoveryPath, postRecovery);
+
+ profileSvc.flush();
+
+ if (shouldLaunch) {
+ Services.startup.createInstanceWithProfile(profile);
+ }
+
+ return profile;
+ } catch (e) {
+ lazy.logConsole.error(
+ "Failed to recover from backup at ",
+ recoveryPath,
+ e
+ );
+ throw e;
+ }
+ }
+
+ /**
+ * Checks for the POST_RECOVERY_FILE_NAME in the current profile directory.
+ * If one exists, instantiates any relevant BackupResource's, and calls
+ * postRecovery() on them with the appropriate entry from the file. Once
+ * this is done, deletes the file.
+ *
+ * The file is deleted even if one of the postRecovery() steps rejects or
+ * fails.
+ *
+ * This function resolves silently if the POST_RECOVERY_FILE_NAME file does
+ * not exist, which should be the majority of cases.
+ *
+ * @param {string} [profilePath=PathUtils.profileDir]
+ * The profile path to look for the POST_RECOVERY_FILE_NAME file. Defaults
+ * to the current profile.
+ * @returns {Promise<undefined>}
+ */
+ async checkForPostRecovery(profilePath = PathUtils.profileDir) {
+ lazy.logConsole.debug(`Checking for post-recovery file in ${profilePath}`);
+ let postRecoveryFile = PathUtils.join(
+ profilePath,
+ BackupService.POST_RECOVERY_FILE_NAME
+ );
+
+ if (!(await IOUtils.exists(postRecoveryFile))) {
+ lazy.logConsole.debug("Did not find post-recovery file.");
+ this.#postRecoveryResolver();
+ return;
+ }
+
+ lazy.logConsole.debug("Found post-recovery file. Loading...");
+
+ try {
+ let postRecovery = await IOUtils.readJSON(postRecoveryFile);
+ for (let resourceKey in postRecovery) {
+ let postRecoveryEntry = postRecovery[resourceKey];
+ let resourceClass = this.#resources.get(resourceKey);
+ if (!resourceClass) {
+ lazy.logConsole.error(
+ `Invalid resource for post-recovery step: ${resourceKey}`
+ );
+ continue;
+ }
+
+ lazy.logConsole.debug(`Running post-recovery step for ${resourceKey}`);
+ await new resourceClass().postRecovery(postRecoveryEntry);
+ lazy.logConsole.debug(`Done post-recovery step for ${resourceKey}`);
+ }
+ } finally {
+ await IOUtils.remove(postRecoveryFile, { ignoreAbsent: true });
+ this.#postRecoveryResolver();
+ }
}
/**
@@ -97,7 +770,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/actors/BackupUIChild.sys.mjs b/browser/components/backup/actors/BackupUIChild.sys.mjs
new file mode 100644
index 0000000000..25d013fa8e
--- /dev/null
+++ b/browser/components/backup/actors/BackupUIChild.sys.mjs
@@ -0,0 +1,54 @@
+/* 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/. */
+
+/**
+ * A JSWindowActor that is responsible for marshalling information between
+ * the BackupService singleton and any registered UI widgets that need to
+ * represent data from that service. Any UI widgets that want to receive
+ * state updates from BackupService should emit a BackupUI:InitWidget
+ * event in a document that this actor pair is registered for.
+ */
+export class BackupUIChild extends JSWindowActorChild {
+ #inittedWidgets = new WeakSet();
+
+ /**
+ * Handles BackupUI:InitWidget custom events fired by widgets that want to
+ * register with BackupUIChild. Firing this event sends a message to the
+ * parent to request the BackupService state which will result in a
+ * `backupServiceState` property of the widget to be set when that state is
+ * received. Subsequent state updates will also cause that state property to
+ * be set.
+ *
+ * @param {Event} event
+ * The BackupUI:InitWidget custom event that the widget fired.
+ */
+ handleEvent(event) {
+ if (event.type == "BackupUI:InitWidget") {
+ this.#inittedWidgets.add(event.target);
+ this.sendAsyncMessage("RequestState");
+ }
+ }
+
+ /**
+ * Handles messages sent by BackupUIParent.
+ *
+ * @param {ReceiveMessageArgument} message
+ * The message received from the BackupUIParent.
+ */
+ receiveMessage(message) {
+ if (message.name == "StateUpdate") {
+ let widgets = ChromeUtils.nondeterministicGetWeakSetKeys(
+ this.#inittedWidgets
+ );
+ for (let widget of widgets) {
+ if (widget.isConnected) {
+ // Note: we might need to switch to using Cu.cloneInto here in the
+ // event that these widgets are embedded in a non-parent-process
+ // context, like in an onboarding card.
+ widget.backupServiceState = message.data.state;
+ }
+ }
+ }
+ }
+}
diff --git a/browser/components/backup/actors/BackupUIParent.sys.mjs b/browser/components/backup/actors/BackupUIParent.sys.mjs
new file mode 100644
index 0000000000..e4d0f3aace
--- /dev/null
+++ b/browser/components/backup/actors/BackupUIParent.sys.mjs
@@ -0,0 +1,82 @@
+/* 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, {
+ BackupService: "resource:///modules/backup/BackupService.sys.mjs",
+});
+
+/**
+ * A JSWindowActor that is responsible for marshalling information between
+ * the BackupService singleton and any registered UI widgets that need to
+ * represent data from that service.
+ */
+export class BackupUIParent extends JSWindowActorParent {
+ /**
+ * A reference to the BackupService singleton instance.
+ *
+ * @type {BackupService}
+ */
+ #bs;
+
+ /**
+ * Create a BackupUIParent instance. If a BackupUIParent is instantiated
+ * before BrowserGlue has a chance to initialize the BackupService, this
+ * constructor will cause it to initialize first.
+ */
+ constructor() {
+ super();
+ // We use init() rather than get(), since it's possible to load
+ // about:preferences before the service has had a chance to init itself
+ // via BrowserGlue.
+ this.#bs = lazy.BackupService.init();
+ }
+
+ /**
+ * Called once the BackupUIParent/BackupUIChild pair have been connected.
+ */
+ actorCreated() {
+ this.#bs.addEventListener("BackupService:StateUpdate", this);
+ }
+
+ /**
+ * Called once the BackupUIParent/BackupUIChild pair have been disconnected.
+ */
+ didDestroy() {
+ this.#bs.removeEventListener("BackupService:StateUpdate", this);
+ }
+
+ /**
+ * Handles events fired by the BackupService.
+ *
+ * @param {Event} event
+ * The event that the BackupService emitted.
+ */
+ handleEvent(event) {
+ if (event.type == "BackupService:StateUpdate") {
+ this.sendState();
+ }
+ }
+
+ /**
+ * Handles messages sent by BackupUIChild.
+ *
+ * @param {ReceiveMessageArgument} message
+ * The message received from the BackupUIChild.
+ */
+ receiveMessage(message) {
+ if (message.name == "RequestState") {
+ this.sendState();
+ }
+ }
+
+ /**
+ * Sends the StateUpdate message to the BackupUIChild, along with the most
+ * recent state object from BackupService.
+ */
+ sendState() {
+ this.sendAsyncMessage("StateUpdate", { state: this.#bs.state });
+ }
+}
diff --git a/browser/components/backup/content/BackupManifest.1.schema.json b/browser/components/backup/content/BackupManifest.1.schema.json
new file mode 100644
index 0000000000..51418988fe
--- /dev/null
+++ b/browser/components/backup/content/BackupManifest.1.schema.json
@@ -0,0 +1,82 @@
+{
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
+ "$id": "file:///BackupManifest.schema.json",
+ "title": "BackupManifest",
+ "description": "A schema for the backup-manifest.json file for profile backups created by the BackupService",
+ "type": "object",
+ "properties": {
+ "version": {
+ "type": "integer",
+ "description": "Version of the backup manifest structure"
+ },
+ "meta": {
+ "type": "object",
+ "description": "Metadata about the backup",
+ "properties": {
+ "date": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Date and time that the backup was created"
+ },
+ "appName": {
+ "type": "string",
+ "description": "Name of the application that the backup was created for."
+ },
+ "appVersion": {
+ "type": "string",
+ "description": "Full version string for the app instance that the backup was created on"
+ },
+ "buildID": {
+ "type": "string",
+ "description": "Build ID for the app instance that the backup was created on"
+ },
+ "profileName": {
+ "type": "string",
+ "description": "The name given to the profile that was backed up"
+ },
+ "machineName": {
+ "type": "string",
+ "description": "The name of the machine that the backup was created on"
+ },
+ "osName": {
+ "type": "string",
+ "description": "The OS name that the backup was created on"
+ },
+ "osVersion": {
+ "type": "string",
+ "description": "The OS version that the backup was created on"
+ },
+ "accountID": {
+ "type": "string",
+ "description": "The ID for the account that the user profile was signed into when backing up. Optional."
+ },
+ "accountEmail": {
+ "type": "string",
+ "description": "The email address for the account that the user profile was signed into when backing up. Optional."
+ },
+ "legacyClientID": {
+ "type": "string",
+ "description": "The legacy telemetry client ID for the profile that the backup was created on."
+ }
+ },
+ "required": [
+ "date",
+ "appName",
+ "appVersion",
+ "buildID",
+ "profileName",
+ "machineName",
+ "osName",
+ "osVersion",
+ "legacyClientID"
+ ]
+ },
+ "resources": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object"
+ }
+ }
+ },
+ "required": ["version", "resources", "meta"]
+}
diff --git a/browser/components/backup/content/backup-settings.mjs b/browser/components/backup/content/backup-settings.mjs
new file mode 100644
index 0000000000..c34d87dbc7
--- /dev/null
+++ b/browser/components/backup/content/backup-settings.mjs
@@ -0,0 +1,47 @@
+/* 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";
+
+/**
+ * The widget for managing the BackupService that is embedded within the main
+ * document of about:settings / about:preferences.
+ */
+export default class BackupSettings extends MozLitElement {
+ static properties = {
+ backupServiceState: { type: Object },
+ };
+
+ /**
+ * Creates a BackupSettings instance and sets the initial default
+ * state.
+ */
+ constructor() {
+ super();
+ this.backupServiceState = {
+ backupInProgress: false,
+ };
+ }
+
+ /**
+ * Dispatches the BackupUI:InitWidget custom event upon being attached to the
+ * DOM, which registers with BackupUIChild for BackupService state updates.
+ */
+ connectedCallback() {
+ super.connectedCallback();
+ this.dispatchEvent(
+ new CustomEvent("BackupUI:InitWidget", { bubbles: true })
+ );
+ }
+
+ render() {
+ return html`<div>
+ Backup in progress:
+ ${this.backupServiceState.backupInProgress ? "Yes" : "No"}
+ </div>`;
+ }
+}
+
+customElements.define("backup-settings", BackupSettings);
diff --git a/browser/components/backup/content/backup-settings.stories.mjs b/browser/components/backup/content/backup-settings.stories.mjs
new file mode 100644
index 0000000000..2a87c361bc
--- /dev/null
+++ b/browser/components/backup/content/backup-settings.stories.mjs
@@ -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/. */
+
+// eslint-disable-next-line import/no-unresolved
+import { html } from "lit.all.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "./backup-settings.mjs";
+
+export default {
+ title: "Domain-specific UI Widgets/Backup/Backup Settings",
+ component: "backup-settings",
+ argTypes: {},
+};
+
+const Template = ({ backupServiceState }) => html`
+ <backup-settings .backupServiceState=${backupServiceState}></backup-settings>
+`;
+
+export const BackingUpNotInProgress = Template.bind({});
+BackingUpNotInProgress.args = {
+ backupServiceState: {
+ backupInProgress: false,
+ },
+};
+
+export const BackingUpInProgress = Template.bind({});
+BackingUpInProgress.args = {
+ backupServiceState: {
+ backupInProgress: true,
+ },
+};
diff --git a/browser/components/backup/content/debug.html b/browser/components/backup/content/debug.html
new file mode 100644
index 0000000000..55034d4a5c
--- /dev/null
+++ b/browser/components/backup/content/debug.html
@@ -0,0 +1,64 @@
+<!-- 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>
+ <p>
+ Clicking "Create backup" will create a backup, and then attempt to
+ show an OS notification with the total time it took to create it. This
+ notification may not appear if your OS has not granted the browser to
+ display notifications.
+ </p>
+ <p id="last-backup-status"></p>
+ <button id="open-backup-folder">Open backups folder</button>
+ <button id="recover-from-staging">
+ Recover from staging folder and launch
+ </button>
+ <p>
+ Clicking "Recover from staging folder and launch" will open a file
+ picker to allow you to select a staging folder. Once selected, a new
+ user profile will be created and the data stores from the staging
+ folder will be copied into that new profile. The new profile will then
+ be launched.
+ </p>
+ <p id="last-recovery-status"></p>
+ </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..7a2cea9640
--- /dev/null
+++ b/browser/components/backup/content/debug.js
@@ -0,0 +1,113 @@
+/* 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);
+ }
+ },
+
+ secondsToHms(seconds) {
+ let h = Math.floor(seconds / 3600);
+ let m = Math.floor((seconds % 3600) / 60);
+ let s = Math.floor((seconds % 3600) % 60);
+ return `${h}h ${m}m ${s}s`;
+ },
+
+ async onButtonClick(button) {
+ switch (button.id) {
+ case "create-backup": {
+ let service = BackupService.get();
+ let lastBackupStatus = document.querySelector("#last-backup-status");
+ lastBackupStatus.textContent = "Creating backup...";
+
+ let then = Cu.now();
+ button.disabled = true;
+ await service.createBackup();
+ let totalTimeSeconds = (Cu.now() - then) / 1000;
+ button.disabled = false;
+ new Notification(`Backup created`, {
+ body: `Total time ${this.secondsToHms(totalTimeSeconds)}`,
+ });
+ lastBackupStatus.textContent = `Backup created - total time: ${this.secondsToHms(
+ totalTimeSeconds
+ )}`;
+ 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;
+ }
+ case "recover-from-staging": {
+ let backupsDir = PathUtils.join(PathUtils.profileDir, "backups");
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(
+ Ci.nsIFilePicker
+ );
+ fp.init(
+ window.browsingContext,
+ "Choose a staging folder",
+ Ci.nsIFilePicker.modeGetFolder
+ );
+ fp.displayDirectory = await IOUtils.getDirectory(backupsDir);
+ let result = await new Promise(resolve => fp.open(resolve));
+ if (result == Ci.nsIFilePicker.returnCancel) {
+ break;
+ }
+
+ let path = fp.file.path;
+ let lastRecoveryStatus = document.querySelector(
+ "#last-recovery-status"
+ );
+ lastRecoveryStatus.textContent = "Recovering from backup...";
+
+ let service = BackupService.get();
+ try {
+ let newProfile = await service.recoverFromBackup(
+ path,
+ true /* shouldLaunch */
+ );
+ lastRecoveryStatus.textContent = `Created profile ${newProfile.name} at ${newProfile.rootDir.path}`;
+ } catch (e) {
+ lastRecoveryStatus.textContent(
+ `Failed to recover: ${e.message} Check the console for the full exception.`
+ );
+ throw e;
+ }
+ }
+ }
+ },
+};
+
+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/backup-ui-actors.rst b/browser/components/backup/docs/backup-ui-actors.rst
new file mode 100644
index 0000000000..eafe59d05b
--- /dev/null
+++ b/browser/components/backup/docs/backup-ui-actors.rst
@@ -0,0 +1,22 @@
+==========================
+Backup UI Actors Reference
+==========================
+
+The ``BackupUIParent`` and ``BackupUIChild`` actors allow UI widgets to access
+the current state of the ``BackupService`` and to subscribe to state updates.
+
+UI widgets that want to subscribe to state updates must ensure that they are
+running in a process and on a page that the ``BackupUIParent/BackupUIChild``
+actor pair are registered for, and then fire a ``BackupUI::InitWidget`` event.
+
+It is expected that these UI widgets will respond to having their
+``backupServiceState`` property set.
+
+.. js:autoclass:: BackupUIParent
+ :members:
+ :private-members:
+
+.. js:autoclass:: BackupUIChild
+.. js::autoattribute:: BackupUIChild#inittedWidgets
+ :members:
+ :private-members:
diff --git a/browser/components/backup/docs/index.rst b/browser/components/backup/docs/index.rst
index 1e201f8f1c..fc8751f9d2 100644
--- a/browser/components/backup/docs/index.rst
+++ b/browser/components/backup/docs/index.rst
@@ -11,3 +11,5 @@ into a single file that can be easily restored from.
:maxdepth: 3
backup-service
+ backup-resources
+ backup-ui-actors
diff --git a/browser/components/backup/jar.mn b/browser/components/backup/jar.mn
new file mode 100644
index 0000000000..94c670afaf
--- /dev/null
+++ b/browser/components/backup/jar.mn
@@ -0,0 +1,11 @@
+# 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
+ content/browser/backup/BackupManifest.1.schema.json (content/BackupManifest.1.schema.json)
+ content/browser/backup/backup-settings.mjs (content/backup-settings.mjs)
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..853ae1d80d 100644
--- a/browser/components/backup/moz.build
+++ b/browser/components/backup/moz.build
@@ -7,12 +7,30 @@
with Files("**"):
BUG_COMPONENT = ("Firefox", "Profiles")
+JAR_MANIFESTS += ["jar.mn"]
+
SPHINX_TREES["docs"] = "docs"
XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+MARIONETTE_MANIFESTS += ["tests/marionette/manifest.toml"]
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+MOCHITEST_CHROME_MANIFESTS += ["tests/chrome/chrome.toml"]
+
+FINAL_TARGET_FILES.actors += [
+ "actors/BackupUIChild.sys.mjs",
+ "actors/BackupUIParent.sys.mjs",
+]
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..29b51b8a7f
--- /dev/null
+++ b/browser/components/backup/resources/AddonsBackupResource.sys.mjs
@@ -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/. */
+
+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 backup(stagingPath, profilePath = PathUtils.profileDir) {
+ // Files and directories to backup.
+ let toCopy = [
+ "extensions.json",
+ "extension-settings.json",
+ "extension-preferences.json",
+ "addonStartup.json.lz4",
+ "browser-extension-data",
+ "extension-store-permissions",
+ ];
+ await BackupResource.copyFiles(profilePath, stagingPath, toCopy);
+
+ // Backup only the XPIs in the extensions directory.
+ let xpiFiles = [];
+ let extensionsXPIDirectoryPath = PathUtils.join(profilePath, "extensions");
+ let xpiDirectoryChildren = await IOUtils.getChildren(
+ extensionsXPIDirectoryPath,
+ {
+ ignoreAbsent: true,
+ }
+ );
+ for (const childFilePath of xpiDirectoryChildren) {
+ if (childFilePath.endsWith(".xpi")) {
+ let childFileName = PathUtils.filename(childFilePath);
+ xpiFiles.push(childFileName);
+ }
+ }
+ // Create the extensions directory in the staging directory.
+ let stagingExtensionsXPIDirectoryPath = PathUtils.join(
+ stagingPath,
+ "extensions"
+ );
+ await IOUtils.makeDirectory(stagingExtensionsXPIDirectoryPath);
+ // Copy all found XPIs to the staging directory.
+ await BackupResource.copyFiles(
+ extensionsXPIDirectoryPath,
+ stagingExtensionsXPIDirectoryPath,
+ xpiFiles
+ );
+
+ // Copy storage sync database.
+ let databases = ["storage-sync-v2.sqlite"];
+ await BackupResource.copySqliteDatabases(
+ profilePath,
+ stagingPath,
+ databases
+ );
+
+ return null;
+ }
+
+ async recover(_manifestEntry, recoveryPath, destProfilePath) {
+ const files = [
+ "extensions.json",
+ "extension-settings.json",
+ "extension-preferences.json",
+ "addonStartup.json.lz4",
+ "browser-extension-data",
+ "extension-store-permissions",
+ "extensions",
+ "storage-sync-v2.sqlite",
+ ];
+ await BackupResource.copyFiles(recoveryPath, destProfilePath, files);
+
+ return null;
+ }
+
+ 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..5be6314a60 100644
--- a/browser/components/backup/resources/BackupResource.sys.mjs
+++ b/browser/components/backup/resources/BackupResource.sys.mjs
@@ -2,8 +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/. */
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
+});
+
// 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 +43,34 @@ 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."
+ );
+ }
+
+ /**
+ * This can be overridden to return a number indicating the priority the
+ * resource should have in the backup order.
+ *
+ * Resources with a higher priority will be backed up first.
+ * The default priority of 0 indicates it can be processed in any order.
+ *
+ * @returns {number}
+ */
+ static get priority() {
+ return 0;
+ }
+
+ /**
* Get the size of a file.
*
* @param {string} filePath - path to a file.
@@ -40,21 +88,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 +127,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;
}
@@ -93,6 +150,75 @@ export class BackupResource {
return size;
}
+ /**
+ * Copy a set of SQLite databases safely from a source directory to a
+ * destination directory. A new read-only connection is opened for each
+ * database, and then a backup is created. If the source database does not
+ * exist, it is ignored.
+ *
+ * @param {string} sourcePath
+ * Path to the source directory of the SQLite databases.
+ * @param {string} destPath
+ * Path to the destination directory where the SQLite databases should be
+ * copied to.
+ * @param {Array<string>} sqliteDatabases
+ * An array of filenames of the SQLite databases to copy.
+ * @returns {Promise<undefined>}
+ */
+ static async copySqliteDatabases(sourcePath, destPath, sqliteDatabases) {
+ for (let fileName of sqliteDatabases) {
+ let sourceFilePath = PathUtils.join(sourcePath, fileName);
+
+ if (!(await IOUtils.exists(sourceFilePath))) {
+ continue;
+ }
+
+ let destFilePath = PathUtils.join(destPath, fileName);
+ let connection;
+
+ try {
+ connection = await lazy.Sqlite.openConnection({
+ path: sourceFilePath,
+ readOnly: true,
+ });
+
+ await connection.backup(
+ destFilePath,
+ BackupResource.SQLITE_PAGES_PER_STEP,
+ BackupResource.SQLITE_STEP_DELAY_MS
+ );
+ } finally {
+ await connection?.close();
+ }
+ }
+ }
+
+ /**
+ * A helper function to copy a set of files from a source directory to a
+ * destination directory. Callers should ensure that the source files can be
+ * copied safely before invoking this function. Files that do not exist will
+ * be ignored. Callers that wish to copy SQLite databases should use
+ * copySqliteDatabases() instead.
+ *
+ * @param {string} sourcePath
+ * Path to the source directory of the files to be copied.
+ * @param {string} destPath
+ * Path to the destination directory where the files should be
+ * copied to.
+ * @param {string[]} fileNames
+ * An array of filenames of the files to copy.
+ * @returns {Promise<undefined>}
+ */
+ static async copyFiles(sourcePath, destPath, fileNames) {
+ for (let fileName of fileNames) {
+ let sourceFilePath = PathUtils.join(sourcePath, fileName);
+ let destFilePath = PathUtils.join(destPath, fileName);
+ if (await IOUtils.exists(sourceFilePath)) {
+ await IOUtils.copy(sourceFilePath, destFilePath, { recursive: true });
+ }
+ }
+ }
+
constructor() {}
/**
@@ -106,4 +232,97 @@ export class BackupResource {
async measure(profilePath) {
throw new Error("BackupResource::measure needs to be overridden.");
}
+
+ /**
+ * Perform a safe copy of the datastores that this resource manages 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");
+ }
+
+ /**
+ * Recovers the datastores that this resource manages from a backup archive
+ * that has been decompressed into the recoveryPath. A pre-existing unlocked
+ * user profile should be available to restore into, and destProfilePath
+ * should point at its location on the file system.
+ *
+ * This method is not expected to be running in an app connected to the
+ * destProfilePath. If the BackupResource needs to run some operations
+ * while attached to the recovery profile, it should do that work inside of
+ * postRecovery(). If data needs to be transferred to postRecovery(), it
+ * should be passed as a JSON serializable object in the return value of this
+ * method.
+ *
+ * @see BackupResource.postRecovery()
+ * @param {object|null} manifestEntry
+ * The object that was returned by the backup() method when the backup was
+ * created. This object can be null if no additional information was needed
+ * for recovery.
+ * @param {string} recoveryPath
+ * The path to the resource directory where the backup archive has been
+ * decompressed.
+ * @param {string} destProfilePath
+ * The path to the profile directory where the backup should be restored to.
+ * @returns {Promise<object|null>}
+ * This should return a JSON serializable object that will be passed to
+ * postRecovery() if any data needs to be passed to it. This object can be
+ * null if no additional information is needed for postRecovery().
+ */
+ // eslint-disable-next-line no-unused-vars
+ async recover(manifestEntry, recoveryPath, destProfilePath) {
+ throw new Error("BackupResource::recover must be overridden");
+ }
+
+ /**
+ * Perform any post-recovery operations that need to be done after the
+ * recovery has been completed and the recovered profile has been attached
+ * to.
+ *
+ * This method is running in an app connected to the recovered profile. The
+ * profile is locked, but this postRecovery method can be used to insert
+ * data into connected datastores, or perform any other operations that can
+ * only occur within the context of the recovered profile.
+ *
+ * @see BackupResource.recover()
+ * @param {object|null} postRecoveryEntry
+ * The object that was returned by the recover() method when the recovery
+ * was originally done. This object can be null if no additional information
+ * is needed for post-recovery.
+ */
+ // eslint-disable-next-line no-unused-vars
+ async postRecovery(postRecoveryEntry) {
+ // no-op by default
+ }
}
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ BackupResource,
+ "SQLITE_PAGES_PER_STEP",
+ "browser.backup.sqlite.pages_per_step",
+ 5
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ BackupResource,
+ "SQLITE_STEP_DELAY_MS",
+ "browser.backup.sqlite.step_delay_ms",
+ 250
+);
diff --git a/browser/components/backup/resources/CookiesBackupResource.sys.mjs b/browser/components/backup/resources/CookiesBackupResource.sys.mjs
new file mode 100644
index 0000000000..87ac27757c
--- /dev/null
+++ b/browser/components/backup/resources/CookiesBackupResource.sys.mjs
@@ -0,0 +1,39 @@
+/* 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 backup(stagingPath, profilePath = PathUtils.profileDir) {
+ await BackupResource.copySqliteDatabases(profilePath, stagingPath, [
+ "cookies.sqlite",
+ ]);
+ return null;
+ }
+
+ async recover(_manifestEntry, recoveryPath, destProfilePath) {
+ await BackupResource.copyFiles(recoveryPath, destProfilePath, [
+ "cookies.sqlite",
+ ]);
+ return null;
+ }
+
+ 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..03a0267f33
--- /dev/null
+++ b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs
@@ -0,0 +1,89 @@
+/* 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 backup(stagingPath, profilePath = PathUtils.profileDir) {
+ const simpleCopyFiles = [
+ "pkcs11.txt",
+ "logins.json",
+ "logins-backup.json",
+ "autofill-profiles.json",
+ "signedInUser.json",
+ ];
+ await BackupResource.copyFiles(profilePath, stagingPath, simpleCopyFiles);
+
+ const sqliteDatabases = ["cert9.db", "key4.db", "credentialstate.sqlite"];
+ await BackupResource.copySqliteDatabases(
+ profilePath,
+ stagingPath,
+ sqliteDatabases
+ );
+
+ return null;
+ }
+
+ async recover(_manifestEntry, recoveryPath, destProfilePath) {
+ const files = [
+ "pkcs11.txt",
+ "logins.json",
+ "logins-backup.json",
+ "autofill-profiles.json",
+ "signedInUser.json",
+ "cert9.db",
+ "key4.db",
+ "credentialstate.sqlite",
+ ];
+ await BackupResource.copyFiles(recoveryPath, destProfilePath, files);
+
+ return null;
+ }
+
+ 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..8e35afc66b
--- /dev/null
+++ b/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs
@@ -0,0 +1,41 @@
+/* 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 backup(stagingPath, profilePath = PathUtils.profileDir) {
+ await BackupResource.copySqliteDatabases(profilePath, stagingPath, [
+ "formhistory.sqlite",
+ ]);
+
+ return null;
+ }
+
+ async recover(_manifestEntry, recoveryPath, destProfilePath) {
+ await BackupResource.copyFiles(recoveryPath, destProfilePath, [
+ "formhistory.sqlite",
+ ]);
+
+ return null;
+ }
+
+ 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..3d66114599
--- /dev/null
+++ b/browser/components/backup/resources/MiscDataBackupResource.sys.mjs
@@ -0,0 +1,154 @@
+/* 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, {
+ ActivityStreamStorage:
+ "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs",
+ ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
+});
+
+const SNIPPETS_TABLE_NAME = "snippets";
+const FILES_FOR_BACKUP = [
+ "enumerate_devices.txt",
+ "protections.sqlite",
+ "SiteSecurityServiceState.bin",
+];
+
+/**
+ * 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 = ["enumerate_devices.txt", "SiteSecurityServiceState.bin"];
+ await BackupResource.copyFiles(profilePath, stagingPath, files);
+
+ const sqliteDatabases = ["protections.sqlite"];
+ await BackupResource.copySqliteDatabases(
+ profilePath,
+ stagingPath,
+ sqliteDatabases
+ );
+
+ // Bug 1890585 - we don't currently have the ability to copy the
+ // chrome-privileged IndexedDB databases under storage/permanent/chrome.
+ // Instead, we'll manually export any IndexedDB data we need to backup
+ // to a separate JSON file.
+
+ // The first IndexedDB database we want to back up is the ActivityStream
+ // one - specifically, the "snippets" table, as this contains information
+ // on ASRouter impressions, blocked messages, message group impressions,
+ // etc.
+ let storage = new lazy.ActivityStreamStorage({
+ storeNames: [SNIPPETS_TABLE_NAME],
+ });
+ let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME);
+ let snippetsObj = {};
+ for (let key of await snippetsTable.getAllKeys()) {
+ snippetsObj[key] = await snippetsTable.get(key);
+ }
+ let snippetsBackupFile = PathUtils.join(
+ stagingPath,
+ "activity-stream-snippets.json"
+ );
+ await IOUtils.writeJSON(snippetsBackupFile, snippetsObj);
+
+ return null;
+ }
+
+ async recover(_manifestEntry, recoveryPath, destProfilePath) {
+ await BackupResource.copyFiles(
+ recoveryPath,
+ destProfilePath,
+ FILES_FOR_BACKUP
+ );
+
+ // The times.json file, the one that powers ProfileAge, works hand in hand
+ // with the Telemetry client ID. We don't want to accidentally _overwrite_
+ // a pre-existing times.json with data from a different profile, because
+ // then the client ID wouldn't match the times.json data anymore.
+ //
+ // The rule that we're following for backups and recoveries is that the
+ // recovered profile always inherits the client ID (and therefore the
+ // times.json) from the profile that _initiated recovery_.
+ //
+ // This means we want to copy the times.json file from the profile that's
+ // currently in use to the destProfilePath.
+ await BackupResource.copyFiles(PathUtils.profileDir, destProfilePath, [
+ "times.json",
+ ]);
+
+ // We also want to write the recoveredFromBackup timestamp now.
+ let profileAge = await lazy.ProfileAge(destProfilePath);
+ await profileAge.recordRecoveredFromBackup();
+
+ // The activity-stream-snippets data will need to be written during the
+ // postRecovery phase, so we'll stash the path to the JSON file in the
+ // post recovery entry.
+ let snippetsBackupFile = PathUtils.join(
+ recoveryPath,
+ "activity-stream-snippets.json"
+ );
+ return { snippetsBackupFile };
+ }
+
+ async postRecovery(postRecoveryEntry) {
+ let { snippetsBackupFile } = postRecoveryEntry;
+
+ // If for some reason, the activity-stream-snippets data file has been
+ // removed already, there's nothing to do.
+ if (!IOUtils.exists(snippetsBackupFile)) {
+ return;
+ }
+
+ let snippetsData = await IOUtils.readJSON(snippetsBackupFile);
+ let storage = new lazy.ActivityStreamStorage({
+ storeNames: [SNIPPETS_TABLE_NAME],
+ });
+ let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME);
+ for (let key in snippetsData) {
+ let value = snippetsData[key];
+ await snippetsTable.set(key, value);
+ }
+ }
+
+ async measure(profilePath = PathUtils.profileDir) {
+ let fullSize = 0;
+
+ for (let filePath of FILES_FOR_BACKUP) {
+ 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..3a9433e67c
--- /dev/null
+++ b/browser/components/backup/resources/PlacesBackupResource.sys.mjs
@@ -0,0 +1,130 @@
+/* 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",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "isBrowsingHistoryEnabled",
+ "places.history.enabled",
+ true
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "isSanitizeOnShutdownEnabled",
+ "privacy.sanitize.sanitizeOnShutdown",
+ false
+);
+
+const BOOKMARKS_BACKUP_FILENAME = "bookmarks.jsonlz4";
+
+/**
+ * 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;
+ }
+
+ static get priority() {
+ return 1;
+ }
+
+ async backup(stagingPath, profilePath = PathUtils.profileDir) {
+ 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_BACKUP_FILENAME
+ );
+ await lazy.BookmarkJSONUtils.exportToFile(bookmarksBackupFile, {
+ compress: true,
+ });
+ return { bookmarksOnly: true };
+ }
+
+ // These are copied in parallel because they're attached[1], and we don't
+ // want them to get out of sync with one another.
+ //
+ // [1]: https://www.sqlite.org/lang_attach.html
+ await Promise.all([
+ BackupResource.copySqliteDatabases(profilePath, stagingPath, [
+ "places.sqlite",
+ ]),
+ BackupResource.copySqliteDatabases(profilePath, stagingPath, [
+ "favicons.sqlite",
+ ]),
+ ]);
+
+ return null;
+ }
+
+ async recover(manifestEntry, recoveryPath, destProfilePath) {
+ if (!manifestEntry) {
+ const simpleCopyFiles = ["places.sqlite", "favicons.sqlite"];
+ await BackupResource.copyFiles(
+ recoveryPath,
+ destProfilePath,
+ simpleCopyFiles
+ );
+ } else {
+ const { bookmarksOnly } = manifestEntry;
+
+ /**
+ * If the recovery file only has bookmarks backed up, pass the file path to postRecovery()
+ * so that we can import all bookmarks into the new profile once it's been launched and restored.
+ */
+ if (bookmarksOnly) {
+ let bookmarksBackupPath = PathUtils.join(
+ recoveryPath,
+ BOOKMARKS_BACKUP_FILENAME
+ );
+ return { bookmarksBackupPath };
+ }
+ }
+
+ return null;
+ }
+
+ async postRecovery(postRecoveryEntry) {
+ if (postRecoveryEntry?.bookmarksBackupPath) {
+ await lazy.BookmarkJSONUtils.importFromFile(
+ postRecoveryEntry.bookmarksBackupPath,
+ {
+ replace: true,
+ }
+ );
+ }
+ }
+
+ 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..80196cab74
--- /dev/null
+++ b/browser/components/backup/resources/PreferencesBackupResource.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";
+
+/**
+ * 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",
+ ];
+ await BackupResource.copyFiles(profilePath, stagingPath, simpleCopyFiles);
+
+ const sqliteDatabases = ["permissions.sqlite", "content-prefs.sqlite"];
+ await BackupResource.copySqliteDatabases(
+ profilePath,
+ stagingPath,
+ sqliteDatabases
+ );
+
+ // 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 recover(_manifestEntry, recoveryPath, destProfilePath) {
+ const simpleCopyFiles = [
+ "prefs.js",
+ "xulstore.json",
+ "permissions.sqlite",
+ "content-prefs.sqlite",
+ "containers.json",
+ "handlers.json",
+ "search.json.mozlz4",
+ "user.js",
+ "chrome",
+ ];
+ await BackupResource.copyFiles(
+ recoveryPath,
+ destProfilePath,
+ simpleCopyFiles
+ );
+
+ 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..d28598944f
--- /dev/null
+++ b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs
@@ -0,0 +1,79 @@
+/* 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 backup(stagingPath, profilePath = PathUtils.profileDir) {
+ let sessionStoreState = lazy.SessionStore.getCurrentState(true);
+ let sessionStorePath = PathUtils.join(stagingPath, "sessionstore.jsonlz4");
+
+ /* Bug 1891854 - remove cookies from session store state if the backup file is
+ * not encrypted. */
+
+ await IOUtils.writeJSON(sessionStorePath, sessionStoreState, {
+ compress: true,
+ });
+ await BackupResource.copyFiles(profilePath, stagingPath, [
+ "sessionstore-backups",
+ ]);
+
+ return null;
+ }
+
+ async recover(_manifestEntry, recoveryPath, destProfilePath) {
+ await BackupResource.copyFiles(recoveryPath, destProfilePath, [
+ "sessionstore.jsonlz4",
+ "sessionstore-backups",
+ ]);
+
+ return null;
+ }
+
+ 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/browser/browser.toml b/browser/components/backup/tests/browser/browser.toml
new file mode 100644
index 0000000000..f222c3b825
--- /dev/null
+++ b/browser/components/backup/tests/browser/browser.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+prefs = [
+ "browser.backup.enabled=true",
+ "browser.backup.preferences.ui.enabled=true",
+]
+
+["browser_settings.js"]
diff --git a/browser/components/backup/tests/browser/browser_settings.js b/browser/components/backup/tests/browser/browser_settings.js
new file mode 100644
index 0000000000..b33dbec7bd
--- /dev/null
+++ b/browser/components/backup/tests/browser/browser_settings.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that the section for controlling backup in about:preferences is
+ * visible, but can also be hidden via a pref.
+ */
+add_task(async function test_preferences_visibility() {
+ await BrowserTestUtils.withNewTab("about:preferences", async browser => {
+ let backupSection =
+ browser.contentDocument.querySelector("#dataBackupGroup");
+ Assert.ok(backupSection, "Found backup preferences section");
+
+ // Our mochitest-browser tests are configured to have the section visible
+ // by default.
+ Assert.ok(
+ BrowserTestUtils.isVisible(backupSection),
+ "Backup section is visible"
+ );
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.backup.preferences.ui.enabled", false]],
+ });
+
+ await BrowserTestUtils.withNewTab("about:preferences", async browser => {
+ let backupSection =
+ browser.contentDocument.querySelector("#dataBackupGroup");
+ Assert.ok(backupSection, "Found backup preferences section");
+
+ Assert.ok(
+ BrowserTestUtils.isHidden(backupSection),
+ "Backup section is now hidden"
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/backup/tests/chrome/chrome.toml b/browser/components/backup/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..b0c01b336f
--- /dev/null
+++ b/browser/components/backup/tests/chrome/chrome.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+skip-if = ["os == 'android'"]
+
+["test_backup_settings.html"]
diff --git a/browser/components/backup/tests/chrome/test_backup_settings.html b/browser/components/backup/tests/chrome/test_backup_settings.html
new file mode 100644
index 0000000000..3619f8a1f4
--- /dev/null
+++ b/browser/components/backup/tests/chrome/test_backup_settings.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for the BackupSettings component</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script
+ src="chrome://browser/content/backup/backup-settings.mjs"
+ type="module"
+></script>
+ <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+ <script>
+
+ const { BrowserTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/BrowserTestUtils.sys.mjs"
+ );
+
+ /**
+ * Tests that adding a backup-settings element to the DOM causes it to
+ * fire a BackupUI:InitWidget event.
+ */
+ add_task(async function test_initWidget() {
+ let settings = document.createElement("backup-settings");
+ let content = document.getElementById("content");
+
+ let sawInitWidget = BrowserTestUtils.waitForEvent(content, "BackupUI:InitWidget");
+ content.appendChild(settings);
+ await sawInitWidget;
+ ok(true, "Saw BackupUI:InitWidget");
+
+ settings.remove();
+ });
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <backup-settings id="test-backup-settings"></backup-settings>
+</div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/browser/components/backup/tests/marionette/http2-ca.pem b/browser/components/backup/tests/marionette/http2-ca.pem
new file mode 100644
index 0000000000..ef5a801720
--- /dev/null
+++ b/browser/components/backup/tests/marionette/http2-ca.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC1DCCAbygAwIBAgIURZvN7yVqFNwThGHASoy1OlOGvOMwDQYJKoZIhvcNAQEL
+BQAwGTEXMBUGA1UEAwwOIEhUVFAyIFRlc3QgQ0EwIhgPMjAxNzAxMDEwMDAwMDBa
+GA8yMDI3MDEwMTAwMDAwMFowGTEXMBUGA1UEAwwOIEhUVFAyIFRlc3QgQ0EwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6iFGoRI4W1kH9braIBjYQPTwT
+2erkNUq07PVoV2wke8HHJajg2B+9sZwGm24ahvJr4q9adWtqZHEIeqVap0WH9xzV
+JJwCfs1D/B5p0DggKZOrIMNJ5Nu5TMJrbA7tFYIP8X6taRqx0wI6iypB7qdw4A8N
+jf1mCyuwJJKkfbmIYXmQsVeQPdI7xeC4SB+oN9OIQ+8nFthVt2Zaqn4CkC86exCA
+BiTMHGyXrZZhW7filhLAdTGjDJHdtMr3/K0dJdMJ77kXDqdo4bN7LyJvaeO0ipVh
+He4m1iWdq5EITjbLHCQELL8Wiy/l8Y+ZFzG4s/5JI/pyUcQx1QOs2hgKNe2NAgMB
+AAGjEDAOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADyDiQnKjsvR
+NrOk0aqgJ8XgK/IgJXFLbAVivjBLwnJGEkwxrFtC14mpTrPuXw9AybhroMjinq4Y
+cNYTFuTE34k0fZEU8d60J/Tpfd1i0EB8+oUPuqOn+N29/LeHPAnkDJdOZye3w0U+
+StAI79WqUYQaKIG7qLnt60dQwBte12uvbuPaB3mREIfDXOKcjLBdZHL1waWjtzUX
+z2E91VIdpvJGfEfXC3fIe1uO9Jh/E9NVWci84+njkNsl+OyBfOJ8T+pV3SHfWedp
+Zbjwh6UTukIuc3mW0rS/qZOa2w3HQaO53BMbluo0w1+cscOepsATld2HHvSiHB+0
+K8SWFRHdBOU=
+-----END CERTIFICATE-----
diff --git a/browser/components/backup/tests/marionette/manifest.toml b/browser/components/backup/tests/marionette/manifest.toml
new file mode 100644
index 0000000000..2982adb693
--- /dev/null
+++ b/browser/components/backup/tests/marionette/manifest.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+run-if = ["buildapp == 'browser'"]
+prefs = ["browser.backup.enabled=true", "browser.backup.log=true"]
+
+["test_backup.py"]
+support-files = ["http2-ca.pem"]
diff --git a/browser/components/backup/tests/marionette/test_backup.py b/browser/components/backup/tests/marionette/test_backup.py
new file mode 100644
index 0000000000..3b11b50ae8
--- /dev/null
+++ b/browser/components/backup/tests/marionette/test_backup.py
@@ -0,0 +1,713 @@
+# 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 json
+import os
+import shutil
+import tempfile
+
+import mozfile
+from marionette_harness import MarionetteTestCase
+
+
+class BackupTest(MarionetteTestCase):
+ # This is the DB key that will be computed for the http2-ca.pem certificate
+ # that's included in a support-file for this test.
+ _cert_db_key = "AAAAAAAAAAAAAAAUAAAAG0Wbze8lahTcE4RhwEqMtTpThrzjMBkxFzAVBgNVBAMMDiBIVFRQMiBUZXN0IENB"
+
+ def setUp(self):
+ MarionetteTestCase.setUp(self)
+ # We need to quit the browser and restart with the browser.backup.log
+ # pref already set to true in order for it to be displayed.
+ self.marionette.quit()
+ self.marionette.instance.prefs = {
+ "browser.backup.log": True,
+ }
+ # Now restart the browser.
+ self.marionette.instance.switch_profile()
+ self.marionette.start_session()
+
+ def test_backup(self):
+ self.marionette.set_context("chrome")
+
+ self.add_test_cookie()
+ self.add_test_login()
+ self.add_test_certificate()
+ self.add_test_saved_address()
+ self.add_test_identity_credential()
+ self.add_test_form_history()
+ self.add_test_activity_stream_snippets_data()
+ self.add_test_protections_data()
+ self.add_test_bookmarks()
+ self.add_test_history()
+ self.add_test_preferences()
+ self.add_test_permissions()
+
+ resourceKeys = self.marionette.execute_script(
+ """
+ const DefaultBackupResources = ChromeUtils.importESModule("resource:///modules/backup/BackupResources.sys.mjs");
+ let resourceKeys = [];
+ for (const resourceName in DefaultBackupResources) {
+ let resource = DefaultBackupResources[resourceName];
+ resourceKeys.push(resource.key);
+ }
+ return resourceKeys;
+ """
+ )
+
+ originalStagingPath = self.marionette.execute_async_script(
+ """
+ const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
+ let bs = BackupService.init();
+ if (!bs) {
+ throw new Error("Could not get initialized BackupService.");
+ }
+
+ let [outerResolve] = arguments;
+ (async () => {
+ let { stagingPath } = await bs.createBackup();
+ if (!stagingPath) {
+ throw new Error("Could not create backup.");
+ }
+ return stagingPath;
+ })().then(outerResolve);
+ """
+ )
+
+ # When we switch over to the recovered profile, the Marionette framework
+ # will blow away the profile directory of the one that we created the
+ # backup on, which ruins our ability to do postRecovery work, since
+ # that relies on the prior profile sticking around. We work around this
+ # by moving the staging folder we got back to the OS temporary
+ # directory, and telling the recovery method to use that instead of the
+ # one from the profile directory.
+ stagingPath = os.path.join(tempfile.gettempdir(), "staging-test")
+ # Delete the destination folder if it exists already
+ shutil.rmtree(stagingPath, ignore_errors=True)
+ shutil.move(originalStagingPath, stagingPath)
+
+ # First, ensure that the staging path exists
+ self.assertTrue(os.path.exists(stagingPath))
+ # Now, ensure that the backup-manifest.json file exists within it.
+ manifestPath = os.path.join(stagingPath, "backup-manifest.json")
+ self.assertTrue(os.path.exists(manifestPath))
+
+ # For now, we just do a cursory check to ensure that for the resources
+ # that are listed in the manifest as having been backed up, that we
+ # have at least one file in their respective staging directories.
+ # We don't check the contents of the files, just that they exist.
+
+ # Read the JSON manifest file
+ with open(manifestPath, "r") as f:
+ manifest = json.load(f)
+
+ # Ensure that the manifest has a "resources" key
+ self.assertIn("resources", manifest)
+ resources = manifest["resources"]
+ self.assertTrue(isinstance(resources, dict))
+ self.assertTrue(len(resources) > 0)
+
+ # We don't have encryption capabilities wired up yet, so we'll check
+ # that all default resources are represented in the manifest.
+ self.assertEqual(len(resources), len(resourceKeys))
+ for resourceKey in resourceKeys:
+ self.assertIn(resourceKey, resources)
+
+ # Iterate the resources dict keys
+ for resourceKey in resources:
+ print("Checking resource: %s" % resourceKey)
+ # Ensure that there are staging directories created for each
+ # resource that was backed up
+ resourceStagingDir = os.path.join(stagingPath, resourceKey)
+ self.assertTrue(os.path.exists(resourceStagingDir))
+
+ # Start a brand new profile, one without any of the data we created or
+ # backed up. This is the one that we'll be starting recovery from.
+ self.marionette.quit()
+ self.marionette.instance.profile = None
+ self.marionette.start_session()
+ self.marionette.set_context("chrome")
+
+ # Recover the created backup into a new profile directory. Also get out
+ # the client ID of this profile, because we're going to want to make
+ # sure that this client ID is inherited by the recovered profile.
+ [
+ newProfileName,
+ newProfilePath,
+ expectedClientID,
+ ] = self.marionette.execute_async_script(
+ """
+ const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs");
+ const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
+ let bs = BackupService.get();
+ if (!bs) {
+ throw new Error("Could not get initialized BackupService.");
+ }
+
+ let [stagingPath, outerResolve] = arguments;
+ (async () => {
+ let newProfileRootPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "recoverFromBackupTest-newProfileRoot"
+ );
+ let newProfile = await bs.recoverFromBackup(stagingPath, false, newProfileRootPath)
+ if (!newProfile) {
+ throw new Error("Could not create recovery profile.");
+ }
+
+ let expectedClientID = await ClientID.getClientID();
+
+ return [newProfile.name, newProfile.rootDir.path, expectedClientID];
+ })().then(outerResolve);
+ """,
+ script_args=[stagingPath],
+ )
+
+ print("Recovery name: %s" % newProfileName)
+ print("Recovery path: %s" % newProfilePath)
+ print("Expected clientID: %s" % expectedClientID)
+
+ self.marionette.quit()
+ originalProfile = self.marionette.instance.profile
+ self.marionette.instance.profile = newProfilePath
+ self.marionette.start_session()
+ self.marionette.set_context("chrome")
+
+ # Ensure that all postRecovery actions have completed.
+ self.marionette.execute_async_script(
+ """
+ const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
+ let bs = BackupService.get();
+ if (!bs) {
+ throw new Error("Could not get initialized BackupService.");
+ }
+
+ let [outerResolve] = arguments;
+ (async () => {
+ await bs.postRecoveryComplete;
+ })().then(outerResolve);
+ """
+ )
+
+ self.verify_recovered_test_cookie()
+ self.verify_recovered_test_login()
+ self.verify_recovered_test_certificate()
+ self.verify_recovered_saved_address()
+ self.verify_recovered_identity_credential()
+ self.verify_recovered_form_history()
+ self.verify_recovered_activity_stream_snippets_data()
+ self.verify_recovered_protections_data()
+ self.verify_recovered_bookmarks()
+ self.verify_recovered_history()
+ self.verify_recovered_preferences()
+ self.verify_recovered_permissions()
+
+ # Now also ensure that the recovered profile inherited the client ID
+ # from the profile that initiated recovery.
+ recoveredClientID = self.marionette.execute_async_script(
+ """
+ const { ClientID } = ChromeUtils.importESModule("resource://gre/modules/ClientID.sys.mjs");
+ let [outerResolve] = arguments;
+ (async () => {
+ return ClientID.getClientID();
+ })().then(outerResolve);
+ """
+ )
+ self.assertEqual(recoveredClientID, expectedClientID)
+
+ # Try not to pollute the profile list by getting rid of the one we just
+ # created.
+ self.marionette.quit()
+ self.marionette.instance.profile = originalProfile
+ self.marionette.start_session()
+ self.marionette.set_context("chrome")
+ self.marionette.execute_script(
+ """
+ let newProfileName = arguments[0];
+ let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+ let profile = profileSvc.getProfileByName(newProfileName);
+ profile.remove(true);
+ profileSvc.flush();
+ """,
+ script_args=[newProfileName],
+ )
+
+ # Cleanup the staging path that we moved
+ mozfile.remove(stagingPath)
+
+ def add_test_cookie(self):
+ self.marionette.execute_async_script(
+ """
+ let [outerResolve] = arguments;
+ (async () => {
+ // We'll just add a single cookie, and then make sure that it shows
+ // up on the other side.
+ Services.cookies.removeAll();
+ Services.cookies.add(
+ ".example.com",
+ "/",
+ "first",
+ "one",
+ false,
+ false,
+ false,
+ Date.now() / 1000 + 1,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTP
+ );
+ })().then(outerResolve);
+ """
+ )
+
+ def verify_recovered_test_cookie(self):
+ cookiesLength = self.marionette.execute_async_script(
+ """
+ let [outerResolve] = arguments;
+ (async () => {
+ let cookies = Services.cookies.getCookiesFromHost("example.com", {});
+ return cookies.length;
+ })().then(outerResolve);
+ """
+ )
+ self.assertEqual(cookiesLength, 1)
+
+ def add_test_login(self):
+ self.marionette.execute_async_script(
+ """
+ let [outerResolve] = arguments;
+ (async () => {
+ // Let's start with adding a single password
+ Services.logins.removeAllLogins();
+
+ const nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ Ci.nsILoginInfo,
+ "init"
+ );
+
+ const login1 = new nsLoginInfo(
+ "https://example.com",
+ "https://example.com",
+ null,
+ "notifyu1",
+ "notifyp1",
+ "user",
+ "pass"
+ );
+ await Services.logins.addLoginAsync(login1);
+ })().then(outerResolve);
+ """
+ )
+
+ def verify_recovered_test_login(self):
+ loginsLength = self.marionette.execute_async_script(
+ """
+ let [outerResolve] = arguments;
+ (async () => {
+ let logins = await Services.logins.searchLoginsAsync({
+ origin: "https://example.com",
+ });
+ return logins.length;
+ })().then(outerResolve);
+ """
+ )
+ self.assertEqual(loginsLength, 1)
+
+ def add_test_certificate(self):
+ certPath = os.path.join(os.path.dirname(__file__), "http2-ca.pem")
+ self.marionette.execute_async_script(
+ """
+ let [certPath, certDbKey, outerResolve] = arguments;
+ (async () => {
+ const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+ );
+
+ let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+
+ if (certDb.findCertByDBKey(certDbKey)) {
+ throw new Error("Should not have this certificate yet!");
+ }
+
+ let certFile = await IOUtils.getFile(certPath);
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(certFile, -1, 0, 0);
+ let data = NetUtil.readInputStreamToString(fstream, fstream.available());
+ fstream.close();
+
+ let pem = data.replace(/-----BEGIN CERTIFICATE-----/, "")
+ .replace(/-----END CERTIFICATE-----/, "")
+ .replace(/[\\r\\n]/g, "");
+ let cert = certDb.addCertFromBase64(pem, "CTu,u,u");
+
+ if (cert.dbKey != certDbKey) {
+ throw new Error("The inserted certificate DB key is unexpected.");
+ }
+ })().then(outerResolve);
+ """,
+ script_args=[certPath, self._cert_db_key],
+ )
+
+ def verify_recovered_test_certificate(self):
+ certExists = self.marionette.execute_async_script(
+ """
+ let [certDbKey, outerResolve] = arguments;
+ (async () => {
+ let certDb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ return certDb.findCertByDBKey(certDbKey) != null;
+ })().then(outerResolve);
+ """,
+ script_args=[self._cert_db_key],
+ )
+ self.assertTrue(certExists)
+
+ def add_test_saved_address(self):
+ self.marionette.execute_async_script(
+ """
+ const { formAutofillStorage } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillStorage.sys.mjs"
+ );
+
+ let [outerResolve] = arguments;
+ (async () => {
+ const TEST_ADDRESS_1 = {
+ "given-name": "John",
+ "additional-name": "R.",
+ "family-name": "Smith",
+ organization: "World Wide Web Consortium",
+ "street-address": "32 Vassar Street\\\nMIT Room 32-G524",
+ "address-level2": "Cambridge",
+ "address-level1": "MA",
+ "postal-code": "02139",
+ country: "US",
+ tel: "+15195555555",
+ email: "user@example.com",
+ };
+ await formAutofillStorage.initialize();
+ formAutofillStorage.addresses.removeAll();
+ await formAutofillStorage.addresses.add(TEST_ADDRESS_1);
+ })().then(outerResolve);
+ """
+ )
+
+ def verify_recovered_saved_address(self):
+ addressesLength = self.marionette.execute_async_script(
+ """
+ const { formAutofillStorage } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofillStorage.sys.mjs"
+ );
+
+ let [outerResolve] = arguments;
+ (async () => {
+ await formAutofillStorage.initialize();
+ let addresses = await formAutofillStorage.addresses.getAll();
+ return addresses.length;
+ })().then(outerResolve);
+ """
+ )
+ self.assertEqual(addressesLength, 1)
+
+ def add_test_identity_credential(self):
+ self.marionette.execute_async_script(
+ """
+ let [outerResolve] = arguments;
+ (async () => {
+ let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"]
+ .getService(Ci.nsIIdentityCredentialStorageService);
+ service.clear();
+
+ let testPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://test.com/"),
+ {}
+ );
+ let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://idp-test.com/"),
+ {}
+ );
+
+ service.setState(
+ testPrincipal,
+ idpPrincipal,
+ "ID",
+ true,
+ true
+ );
+
+ })().then(outerResolve);
+ """
+ )
+
+ def verify_recovered_identity_credential(self):
+ [registered, allowLogout] = self.marionette.execute_async_script(
+ """
+ let [outerResolve] = arguments;
+ (async () => {
+ let service = Cc["@mozilla.org/browser/identity-credential-storage-service;1"]
+ .getService(Ci.nsIIdentityCredentialStorageService);
+
+ let testPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://test.com/"),
+ {}
+ );
+ let idpPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI("https://idp-test.com/"),
+ {}
+ );
+
+ let registered = {};
+ let allowLogout = {};
+
+ service.getState(
+ testPrincipal,
+ idpPrincipal,
+ "ID",
+ registered,
+ allowLogout
+ );
+
+ return [registered.value, allowLogout.value];
+ })().then(outerResolve);
+ """
+ )
+ self.assertTrue(registered)
+ self.assertTrue(allowLogout)
+
+ def add_test_form_history(self):
+ self.marionette.execute_async_script(
+ """
+ const { FormHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/FormHistory.sys.mjs"
+ );
+
+ let [outerResolve] = arguments;
+ (async () => {
+ await FormHistory.update({
+ op: "add",
+ fieldname: "some-test-field",
+ value: "I was recovered!",
+ timesUsed: 1,
+ firstUsed: 0,
+ lastUsed: 0,
+ });
+
+ })().then(outerResolve);
+ """
+ )
+
+ def verify_recovered_form_history(self):
+ formHistoryResultsLength = self.marionette.execute_async_script(
+ """
+ const { FormHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/FormHistory.sys.mjs"
+ );
+
+ let [outerResolve] = arguments;
+ (async () => {
+ let results = await FormHistory.search(
+ ["guid"],
+ { fieldname: "some-test-field" }
+ );
+ return results.length;
+ })().then(outerResolve);
+ """
+ )
+ self.assertEqual(formHistoryResultsLength, 1)
+
+ def add_test_activity_stream_snippets_data(self):
+ self.marionette.execute_async_script(
+ """
+ const { ActivityStreamStorage } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs",
+ );
+ const SNIPPETS_TABLE_NAME = "snippets";
+
+ let [outerResolve] = arguments;
+ (async () => {
+ let storage = new ActivityStreamStorage({
+ storeNames: [SNIPPETS_TABLE_NAME],
+ });
+ let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME);
+ await snippetsTable.set("backup-test", "some-test-value");
+ })().then(outerResolve);
+ """
+ )
+
+ def verify_recovered_activity_stream_snippets_data(self):
+ snippetsResult = self.marionette.execute_async_script(
+ """
+ const { ActivityStreamStorage } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs",
+ );
+ const SNIPPETS_TABLE_NAME = "snippets";
+
+ let [outerResolve] = arguments;
+ (async () => {
+ let storage = new ActivityStreamStorage({
+ storeNames: [SNIPPETS_TABLE_NAME],
+ });
+ let snippetsTable = await storage.getDbTable(SNIPPETS_TABLE_NAME);
+ return await snippetsTable.get("backup-test");
+ })().then(outerResolve);
+ """
+ )
+ self.assertEqual(snippetsResult, "some-test-value")
+
+ def add_test_protections_data(self):
+ self.marionette.execute_async_script(
+ """
+ const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"]
+ .getService(Ci.nsITrackingDBService);
+
+ let [outerResolve] = arguments;
+ (async () => {
+ let entry = {
+ "https://test.com": [
+ [Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT, true, 1],
+ ],
+ };
+ await TrackingDBService.clearAll();
+ await TrackingDBService.saveEvents(JSON.stringify(entry));
+ })().then(outerResolve);
+ """
+ )
+
+ def verify_recovered_protections_data(self):
+ eventsSum = self.marionette.execute_async_script(
+ """
+ const TrackingDBService = Cc["@mozilla.org/tracking-db-service;1"]
+ .getService(Ci.nsITrackingDBService);
+
+ let [outerResolve] = arguments;
+ (async () => {
+ return TrackingDBService.sumAllEvents();
+ })().then(outerResolve);
+ """
+ )
+ self.assertEqual(eventsSum, 1)
+
+ def add_test_bookmarks(self):
+ self.marionette.execute_async_script(
+ """
+ const { PlacesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesUtils.sys.mjs"
+ );
+
+ let [outerResolve] = arguments;
+ (async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "Some test page",
+ url: Services.io.newURI("https://www.backup.test/"),
+ });
+ })().then(outerResolve);
+ """
+ )
+
+ def verify_recovered_bookmarks(self):
+ bookmarkExists = self.marionette.execute_async_script(
+ """
+ const { PlacesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesUtils.sys.mjs"
+ );
+
+ let [outerResolve] = arguments;
+ (async () => {
+ let url = Services.io.newURI("https://www.backup.test/");
+ let bookmark = await PlacesUtils.bookmarks.fetch({ url });
+ return bookmark != null;
+ })().then(outerResolve);
+ """
+ )
+ self.assertTrue(bookmarkExists)
+
+ def add_test_history(self):
+ self.marionette.execute_async_script(
+ """
+ const { PlacesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesUtils.sys.mjs"
+ );
+
+ let [outerResolve] = arguments;
+ (async () => {
+ await PlacesUtils.history.clear();
+
+ let entry = {
+ url: "http://my-restored-history.com",
+ visits: [{ transition: PlacesUtils.history.TRANSITION_LINK }],
+ };
+
+ await PlacesUtils.history.insertMany([entry]);
+ })().then(outerResolve);
+ """
+ )
+
+ def verify_recovered_history(self):
+ historyExists = self.marionette.execute_async_script(
+ """
+ const { PlacesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesUtils.sys.mjs"
+ );
+
+ let [outerResolve] = arguments;
+ (async () => {
+ let entry = await PlacesUtils.history.fetch("http://my-restored-history.com");
+ return entry != null;
+ })().then(outerResolve);
+ """
+ )
+ self.assertTrue(historyExists)
+
+ def add_test_preferences(self):
+ self.marionette.execute_script(
+ """
+ Services.prefs.setBoolPref("test-pref-for-backup", true)
+ """
+ )
+
+ def verify_recovered_preferences(self):
+ prefExists = self.marionette.execute_script(
+ """
+ return Services.prefs.getBoolPref("test-pref-for-backup", false);
+ """
+ )
+ self.assertTrue(prefExists)
+
+ def add_test_permissions(self):
+ self.marionette.execute_script(
+ """
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://test-permission-site.com"
+ );
+ Services.perms.addFromPrincipal(
+ principal,
+ "desktop-notification",
+ Services.perms.ALLOW_ACTION
+ );
+ """
+ )
+
+ def verify_recovered_permissions(self):
+ permissionExists = self.marionette.execute_script(
+ """
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://test-permission-site.com"
+ );
+ let perms = Services.perms.getAllForPrincipal(principal);
+ if (perms.length != 1) {
+ throw new Error("Got an unexpected number of permissions");
+ }
+ return perms[0].type == "desktop-notification"
+ """
+ )
+ self.assertTrue(permissionExists)
diff --git a/browser/components/backup/tests/xpcshell/data/test_xulstore.json b/browser/components/backup/tests/xpcshell/data/test_xulstore.json
index 0d0890ab16..e4ae6f1f66 100644
--- a/browser/components/backup/tests/xpcshell/data/test_xulstore.json
+++ b/browser/components/backup/tests/xpcshell/data/test_xulstore.json
@@ -9,7 +9,6 @@
"sizemode": "normal"
},
"sidebar-box": {
- "sidebarcommand": "viewBookmarksSidebar",
"width": "323",
"style": "width: 323px;"
},
diff --git a/browser/components/backup/tests/xpcshell/head.js b/browser/components/backup/tests/xpcshell/head.js
new file mode 100644
index 0000000000..e5ed32fb63
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/head.js
@@ -0,0 +1,173 @@
+/* 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;
+ }
+ static get priority() {
+ return 1;
+ }
+}
+
+/**
+ * Yet another fake backup resource class to test with.
+ */
+class FakeBackupResource3 extends BackupResource {
+ static get key() {
+ return "fake3";
+ }
+ static get requiresEncryption() {
+ return false;
+ }
+ static get priority() {
+ return 2;
+ }
+}
+
+/**
+ * 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_AddonsBackupResource.js b/browser/components/backup/tests/xpcshell/test_AddonsBackupResource.js
new file mode 100644
index 0000000000..d1c47ecdb0
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_AddonsBackupResource.js
@@ -0,0 +1,416 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonsBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/AddonsBackupResource.sys.mjs"
+);
+
+/**
+ * Tests that we can measure the size of all the addons & extensions data.
+ */
+add_task(async function test_measure() {
+ 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
+ * and moz-extension IndexedDB databases not existing.
+ */
+add_task(async function test_measure_missing_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 permissions data"
+ );
+
+ let extensionsStorageSizeMeasurement =
+ Glean.browserBackup.extensionsStorageSize.testGetValue();
+ Assert.equal(
+ extensionsStorageSizeMeasurement,
+ null,
+ "Should NOT have collected a measurement for the missing storage data"
+ );
+});
+
+/**
+ * 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 addonsBackupResource = new AddonsBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "AddonsBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "AddonsBackupResource-staging-test"
+ );
+
+ const simpleCopyFiles = [
+ { path: "extensions.json" },
+ { path: "extension-settings.json" },
+ { path: "extension-preferences.json" },
+ { path: "addonStartup.json.lz4" },
+ {
+ path: [
+ "browser-extension-data",
+ "{11aa1234-f111-1234-abcd-a9b8c7654d32}",
+ ],
+ },
+ { path: ["extension-store-permissions", "data.safe.bin"] },
+ { path: ["extensions", "{11aa1234-f111-1234-abcd-a9b8c7654d32}.xpi"] },
+ ];
+ await createTestFiles(sourcePath, simpleCopyFiles);
+
+ const junkFiles = [{ path: ["extensions", "junk"] }];
+ await createTestFiles(sourcePath, junkFiles);
+
+ // Create a fake storage-sync-v2 database file. We don't expect this to
+ // be copied to the staging directory in this test due to our stubbing
+ // of the backup method, so we don't include it in `simpleCopyFiles`.
+ await createTestFiles(sourcePath, [{ path: "storage-sync-v2.sqlite" }]);
+
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+
+ let manifestEntry = await addonsBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+ Assert.equal(
+ manifestEntry,
+ null,
+ "AddonsBackupResource.backup should return null as its ManifestEntry"
+ );
+
+ await assertFilesExist(stagingPath, simpleCopyFiles);
+
+ let junkFile = PathUtils.join(stagingPath, "extensions", "junk");
+ Assert.equal(
+ await IOUtils.exists(junkFile),
+ false,
+ `${junkFile} should not exist in the staging folder`
+ );
+
+ // Make sure storage-sync-v2 database is backed up.
+ Assert.ok(
+ fakeConnection.backup.calledOnce,
+ "Called backup the expected number of times for all connections"
+ );
+ Assert.ok(
+ fakeConnection.backup.calledWith(
+ PathUtils.join(stagingPath, "storage-sync-v2.sqlite")
+ ),
+ "Called backup on the storage-sync-v2 Sqlite connection"
+ );
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
+
+/**
+ * Test that the recover method correctly copies items from the recovery
+ * directory into the destination profile directory.
+ */
+add_task(async function test_recover() {
+ let addonsBackupResource = new AddonsBackupResource();
+ let recoveryPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "addonsBackupResource-recovery-test"
+ );
+ let destProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "addonsBackupResource-test-profile"
+ );
+
+ const files = [
+ { path: "extensions.json" },
+ { path: "extension-settings.json" },
+ { path: "extension-preferences.json" },
+ { path: "addonStartup.json.lz4" },
+ { path: "storage-sync-v2.sqlite" },
+ { path: ["browser-extension-data", "addon@darkreader.org.xpi", "data"] },
+ { path: ["extensions", "addon@darkreader.org.xpi"] },
+ { path: ["extension-store-permissions", "data.safe.bin"] },
+ ];
+ await createTestFiles(recoveryPath, files);
+
+ // The backup method is expected to have returned a null ManifestEntry
+ let postRecoveryEntry = await addonsBackupResource.recover(
+ null /* manifestEntry */,
+ recoveryPath,
+ destProfilePath
+ );
+ Assert.equal(
+ postRecoveryEntry,
+ null,
+ "AddonsBackupResource.recover should return null as its post " +
+ "recovery entry"
+ );
+
+ await assertFilesExist(destProfilePath, files);
+
+ await maybeRemovePath(recoveryPath);
+ await maybeRemovePath(destProfilePath);
+});
diff --git a/browser/components/backup/tests/xpcshell/test_BackupResource.js b/browser/components/backup/tests/xpcshell/test_BackupResource.js
new file mode 100644
index 0000000000..42cda918f9
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_BackupResource.js
@@ -0,0 +1,250 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { bytesToFuzzyKilobytes } = ChromeUtils.importESModule(
+ "resource:///modules/backup/BackupResource.sys.mjs"
+);
+
+const EXPECTED_KILOBYTES_FOR_XULSTORE = 1;
+
+/**
+ * Tests that BackupService.getFileSize will get the size of a file in kilobytes.
+ */
+add_task(async function test_getFileSize() {
+ let file = do_get_file("data/test_xulstore.json");
+
+ let testFilePath = PathUtils.join(PathUtils.profileDir, "test_xulstore.json");
+
+ await IOUtils.copy(file.path, PathUtils.profileDir);
+
+ let size = await BackupResource.getFileSize(testFilePath);
+
+ Assert.equal(
+ size,
+ EXPECTED_KILOBYTES_FOR_XULSTORE,
+ "Size of the test_xulstore.json is rounded up to the nearest kilobyte."
+ );
+
+ await IOUtils.remove(testFilePath);
+});
+
+/**
+ * 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");
+
+ // Create a test directory with the test json file in it.
+ let testDir = PathUtils.join(PathUtils.profileDir, "testDir");
+ await IOUtils.makeDirectory(testDir);
+ await IOUtils.copy(file.path, testDir);
+
+ // Create another test directory inside of that one.
+ let nestedTestDir = PathUtils.join(testDir, "testDir");
+ await IOUtils.makeDirectory(nestedTestDir);
+ await IOUtils.copy(file.path, nestedTestDir);
+
+ let size = await BackupResource.getDirectorySize(testDir);
+
+ Assert.equal(
+ size,
+ EXPECTED_KILOBYTES_FOR_XULSTORE * 2,
+ `Total size of the directory is rounded up to the nearest kilobyte
+ and is equal to twice the size of the test_xulstore.json file`
+ );
+
+ 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");
+});
+
+/**
+ * Tests that BackupResource.copySqliteDatabases will call `backup` on a new
+ * read-only connection on each database file.
+ */
+add_task(async function test_copySqliteDatabases() {
+ let sandbox = sinon.createSandbox();
+ const SQLITE_PAGES_PER_STEP_PREF = "browser.backup.sqlite.pages_per_step";
+ const SQLITE_STEP_DELAY_MS_PREF = "browser.backup.sqlite.step_delay_ms";
+ const DEFAULT_SQLITE_PAGES_PER_STEP = Services.prefs.getIntPref(
+ SQLITE_PAGES_PER_STEP_PREF
+ );
+ const DEFAULT_SQLITE_STEP_DELAY_MS = Services.prefs.getIntPref(
+ SQLITE_STEP_DELAY_MS_PREF
+ );
+
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "BackupResource-source-test"
+ );
+ let destPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "BackupResource-dest-test"
+ );
+ let pretendDatabases = ["places.sqlite", "favicons.sqlite"];
+ await createTestFiles(
+ sourcePath,
+ pretendDatabases.map(f => ({ path: f }))
+ );
+
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+
+ await BackupResource.copySqliteDatabases(
+ sourcePath,
+ destPath,
+ pretendDatabases
+ );
+
+ Assert.ok(
+ Sqlite.openConnection.calledTwice,
+ "Sqlite.openConnection called twice"
+ );
+ Assert.ok(
+ Sqlite.openConnection.firstCall.calledWith({
+ path: PathUtils.join(sourcePath, "places.sqlite"),
+ readOnly: true,
+ }),
+ "openConnection called with places.sqlite as read-only"
+ );
+ Assert.ok(
+ Sqlite.openConnection.secondCall.calledWith({
+ path: PathUtils.join(sourcePath, "favicons.sqlite"),
+ readOnly: true,
+ }),
+ "openConnection called with favicons.sqlite as read-only"
+ );
+
+ Assert.ok(
+ fakeConnection.backup.calledTwice,
+ "backup on an Sqlite connection called twice"
+ );
+ Assert.ok(
+ fakeConnection.backup.firstCall.calledWith(
+ PathUtils.join(destPath, "places.sqlite"),
+ DEFAULT_SQLITE_PAGES_PER_STEP,
+ DEFAULT_SQLITE_STEP_DELAY_MS
+ ),
+ "backup called with places.sqlite to the destination path with the right " +
+ "pages per step and step delay"
+ );
+ Assert.ok(
+ fakeConnection.backup.secondCall.calledWith(
+ PathUtils.join(destPath, "favicons.sqlite"),
+ DEFAULT_SQLITE_PAGES_PER_STEP,
+ DEFAULT_SQLITE_STEP_DELAY_MS
+ ),
+ "backup called with favicons.sqlite to the destination path with the " +
+ "right pages per step and step delay"
+ );
+
+ Assert.ok(
+ fakeConnection.close.calledTwice,
+ "close on an Sqlite connection called twice"
+ );
+
+ // Now check that we can override the default pages per step and step delay.
+ fakeConnection.backup.resetHistory();
+ const NEW_SQLITE_PAGES_PER_STEP = 10;
+ const NEW_SQLITE_STEP_DELAY_MS = 500;
+ Services.prefs.setIntPref(
+ SQLITE_PAGES_PER_STEP_PREF,
+ NEW_SQLITE_PAGES_PER_STEP
+ );
+ Services.prefs.setIntPref(
+ SQLITE_STEP_DELAY_MS_PREF,
+ NEW_SQLITE_STEP_DELAY_MS
+ );
+ await BackupResource.copySqliteDatabases(
+ sourcePath,
+ destPath,
+ pretendDatabases
+ );
+ Assert.ok(
+ fakeConnection.backup.calledTwice,
+ "backup on an Sqlite connection called twice"
+ );
+ Assert.ok(
+ fakeConnection.backup.firstCall.calledWith(
+ PathUtils.join(destPath, "places.sqlite"),
+ NEW_SQLITE_PAGES_PER_STEP,
+ NEW_SQLITE_STEP_DELAY_MS
+ ),
+ "backup called with places.sqlite to the destination path with the right " +
+ "pages per step and step delay"
+ );
+ Assert.ok(
+ fakeConnection.backup.secondCall.calledWith(
+ PathUtils.join(destPath, "favicons.sqlite"),
+ NEW_SQLITE_PAGES_PER_STEP,
+ NEW_SQLITE_STEP_DELAY_MS
+ ),
+ "backup called with favicons.sqlite to the destination path with the " +
+ "right pages per step and step delay"
+ );
+
+ await maybeRemovePath(sourcePath);
+ await maybeRemovePath(destPath);
+ sandbox.restore();
+});
+
+/**
+ * Tests that BackupResource.copyFiles will copy files from one directory to
+ * another.
+ */
+add_task(async function test_copyFiles() {
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "BackupResource-source-test"
+ );
+ let destPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "BackupResource-dest-test"
+ );
+
+ const testFiles = [
+ { path: "file1.txt" },
+ { path: ["some", "nested", "file", "file2.txt"] },
+ { path: "file3.txt" },
+ ];
+
+ await createTestFiles(sourcePath, testFiles);
+
+ await BackupResource.copyFiles(sourcePath, destPath, [
+ "file1.txt",
+ "some",
+ "file3.txt",
+ "does-not-exist.txt",
+ ]);
+
+ await assertFilesExist(destPath, testFiles);
+ Assert.ok(
+ !(await IOUtils.exists(PathUtils.join(destPath, "does-not-exist.txt"))),
+ "does-not-exist.txt wasn't somehow written to."
+ );
+
+ await maybeRemovePath(sourcePath);
+ await maybeRemovePath(destPath);
+});
diff --git a/browser/components/backup/tests/xpcshell/test_BackupService.js b/browser/components/backup/tests/xpcshell/test_BackupService.js
new file mode 100644
index 0000000000..33fb9fbb99
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_BackupService.js
@@ -0,0 +1,451 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { JsonSchemaValidator } = ChromeUtils.importESModule(
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs"
+);
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+
+add_setup(function () {
+ // Much of this setup is copied from toolkit/profile/xpcshell/head.js. It is
+ // needed in order to put the xpcshell test environment into the state where
+ // it thinks its profile is the one pointed at by
+ // nsIToolkitProfileService.currentProfile.
+ let gProfD = do_get_profile();
+ let gDataHome = gProfD.clone();
+ gDataHome.append("data");
+ gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ let gDataHomeLocal = gProfD.clone();
+ gDataHomeLocal.append("local");
+ gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService(
+ Ci.nsIXREDirProvider
+ );
+ xreDirProvider.setUserDataDirectory(gDataHome, false);
+ xreDirProvider.setUserDataDirectory(gDataHomeLocal, true);
+
+ let profileSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(
+ Ci.nsIToolkitProfileService
+ );
+
+ let createdProfile = {};
+ let didCreate = profileSvc.selectStartupProfile(
+ ["xpcshell"],
+ false,
+ AppConstants.UPDATE_CHANNEL,
+ "",
+ {},
+ {},
+ createdProfile
+ );
+ Assert.ok(didCreate, "Created a testing profile and set it to current.");
+ Assert.equal(
+ profileSvc.currentProfile,
+ createdProfile.value,
+ "Profile set to current"
+ );
+});
+
+/**
+ * A utility function for testing BackupService.createBackup. This helper
+ * function:
+ *
+ * 1. Ensures that `backup` will be called on BackupResources with the service
+ * 2. Ensures that a backup-manifest.json will be written and contain the
+ * ManifestEntry data returned by each BackupResource.
+ * 3. Ensures that a `staging` folder will be written to and renamed properly
+ * once the backup creation is complete.
+ *
+ * Once this is done, a task function can be run. The task function is passed
+ * the parsed backup-manifest.json object as its only argument.
+ *
+ * @param {object} sandbox
+ * The Sinon sandbox to be used stubs and mocks. The test using this helper
+ * is responsible for creating and resetting this sandbox.
+ * @param {Function} taskFn
+ * A function that is run once all default checks are done on the manifest
+ * and staging folder. After this function returns, the staging folder will
+ * be cleaned up.
+ * @returns {Promise<undefined>}
+ */
+async function testCreateBackupHelper(sandbox, taskFn) {
+ const EXPECTED_CLIENT_ID = await ClientID.getClientID();
+
+ let fake1ManifestEntry = { fake1: "hello from 1" };
+ sandbox
+ .stub(FakeBackupResource1.prototype, "backup")
+ .resolves(fake1ManifestEntry);
+
+ sandbox
+ .stub(FakeBackupResource2.prototype, "backup")
+ .rejects(new Error("Some failure to backup"));
+
+ let fake3ManifestEntry = { fake3: "hello from 3" };
+ sandbox
+ .stub(FakeBackupResource3.prototype, "backup")
+ .resolves(fake3ManifestEntry);
+
+ let bs = new BackupService({
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ });
+
+ let fakeProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "createBackupTest"
+ );
+
+ await bs.createBackup({ profilePath: fakeProfilePath });
+
+ // We expect the staging folder to exist then be renamed under the fakeProfilePath.
+ // We should also find a folder for each fake BackupResource.
+ let backupsFolderPath = PathUtils.join(fakeProfilePath, "backups");
+ let stagingPath = PathUtils.join(backupsFolderPath, "staging");
+
+ // For now, we expect a single backup only to be saved.
+ let backups = await IOUtils.getChildren(backupsFolderPath);
+ Assert.equal(
+ backups.length,
+ 1,
+ "There should only be 1 backup in the backups folder"
+ );
+
+ let renamedFilename = await PathUtils.filename(backups[0]);
+ let expectedFormatRegex = /^\d{4}(-\d{2}){2}T(\d{2}-){2}\d{2}Z$/;
+ Assert.ok(
+ renamedFilename.match(expectedFormatRegex),
+ "Renamed staging folder should have format YYYY-MM-DDTHH-mm-ssZ"
+ );
+
+ let stagingPathRenamed = PathUtils.join(backupsFolderPath, renamedFilename);
+
+ for (let backupResourceClass of [
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ ]) {
+ let expectedResourceFolderBeforeRename = PathUtils.join(
+ stagingPath,
+ backupResourceClass.key
+ );
+ let expectedResourceFolderAfterRename = PathUtils.join(
+ stagingPathRenamed,
+ backupResourceClass.key
+ );
+
+ Assert.ok(
+ await IOUtils.exists(expectedResourceFolderAfterRename),
+ `BackupResource folder exists for ${backupResourceClass.key} after rename`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.backup.calledOnce,
+ `Backup was called for ${backupResourceClass.key}`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.backup.calledWith(
+ expectedResourceFolderBeforeRename,
+ fakeProfilePath
+ ),
+ `Backup was called in the staging folder for ${backupResourceClass.key} before rename`
+ );
+ }
+
+ // Check that resources were called from highest to lowest backup priority.
+ sinon.assert.callOrder(
+ FakeBackupResource3.prototype.backup,
+ FakeBackupResource2.prototype.backup,
+ FakeBackupResource1.prototype.backup
+ );
+
+ let manifestPath = PathUtils.join(
+ stagingPathRenamed,
+ BackupService.MANIFEST_FILE_NAME
+ );
+
+ Assert.ok(await IOUtils.exists(manifestPath), "Manifest file exists");
+ let manifest = await IOUtils.readJSON(manifestPath);
+
+ let schema = await BackupService.MANIFEST_SCHEMA;
+ let validationResult = JsonSchemaValidator.validate(manifest, schema);
+ Assert.ok(validationResult.valid, "Schema matches manifest");
+ Assert.deepEqual(
+ Object.keys(manifest.resources).sort(),
+ ["fake1", "fake3"],
+ "Manifest contains all expected BackupResource keys"
+ );
+ Assert.deepEqual(
+ manifest.resources.fake1,
+ fake1ManifestEntry,
+ "Manifest contains the expected entry for FakeBackupResource1"
+ );
+ Assert.deepEqual(
+ manifest.resources.fake3,
+ fake3ManifestEntry,
+ "Manifest contains the expected entry for FakeBackupResource3"
+ );
+ Assert.equal(
+ manifest.meta.legacyClientID,
+ EXPECTED_CLIENT_ID,
+ "The client ID was stored properly."
+ );
+
+ taskFn(manifest);
+
+ // 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 });
+}
+
+/**
+ * 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. Tests in the signed-out state.
+ */
+add_task(async function test_createBackup_signed_out() {
+ let sandbox = sinon.createSandbox();
+
+ sandbox
+ .stub(UIState, "get")
+ .returns({ status: UIState.STATUS_NOT_CONFIGURED });
+ await testCreateBackupHelper(sandbox, manifest => {
+ Assert.equal(
+ manifest.meta.accountID,
+ undefined,
+ "Account ID should be undefined."
+ );
+ Assert.equal(
+ manifest.meta.accountEmail,
+ undefined,
+ "Account email should be undefined."
+ );
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * 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. Tests in the signed-in state.
+ */
+add_task(async function test_createBackup_signed_in() {
+ let sandbox = sinon.createSandbox();
+
+ const TEST_UID = "ThisIsMyTestUID";
+ const TEST_EMAIL = "foxy@mozilla.org";
+
+ sandbox.stub(UIState, "get").returns({
+ status: UIState.STATUS_SIGNED_IN,
+ uid: TEST_UID,
+ email: TEST_EMAIL,
+ });
+
+ await testCreateBackupHelper(sandbox, manifest => {
+ Assert.equal(
+ manifest.meta.accountID,
+ TEST_UID,
+ "Account ID should be set properly."
+ );
+ Assert.equal(
+ manifest.meta.accountEmail,
+ TEST_EMAIL,
+ "Account email should be set properly."
+ );
+ });
+
+ sandbox.restore();
+});
+
+/**
+ * Creates a directory that looks a lot like a decompressed backup archive,
+ * and then tests that BackupService.recoverFromBackup can create a new profile
+ * and recover into it.
+ */
+add_task(async function test_recoverFromBackup() {
+ let sandbox = sinon.createSandbox();
+ let fakeEntryMap = new Map();
+ let backupResourceClasses = [
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ ];
+
+ let i = 1;
+ for (let backupResourceClass of backupResourceClasses) {
+ let fakeManifestEntry = { [`fake${i}`]: `hello from backup - ${i}` };
+ sandbox
+ .stub(backupResourceClass.prototype, "backup")
+ .resolves(fakeManifestEntry);
+
+ let fakePostRecoveryEntry = { [`fake${i}`]: `hello from recover - ${i}` };
+ sandbox
+ .stub(backupResourceClass.prototype, "recover")
+ .resolves(fakePostRecoveryEntry);
+
+ fakeEntryMap.set(backupResourceClass, {
+ manifestEntry: fakeManifestEntry,
+ postRecoveryEntry: fakePostRecoveryEntry,
+ });
+
+ ++i;
+ }
+
+ let bs = new BackupService({
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ });
+
+ let oldProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "recoverFromBackupTest"
+ );
+ let newProfileRootPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "recoverFromBackupTest-newProfileRoot"
+ );
+
+ let { stagingPath } = await bs.createBackup({ profilePath: oldProfilePath });
+
+ let testTelemetryStateObject = {
+ clientID: "ed209123-04a1-04a1-04a1-c0ffeec0ffee",
+ };
+ await IOUtils.writeJSON(
+ PathUtils.join(PathUtils.profileDir, "datareporting", "state.json"),
+ testTelemetryStateObject
+ );
+
+ let profile = await bs.recoverFromBackup(
+ stagingPath,
+ false /* shouldLaunch */,
+ newProfileRootPath
+ );
+ Assert.ok(profile, "An nsIToolkitProfile was created.");
+ let newProfilePath = profile.rootDir.path;
+
+ let postRecoveryFilePath = PathUtils.join(
+ newProfilePath,
+ "post-recovery.json"
+ );
+ let postRecovery = await IOUtils.readJSON(postRecoveryFilePath);
+
+ for (let backupResourceClass of backupResourceClasses) {
+ let expectedResourceFolder = PathUtils.join(
+ stagingPath,
+ backupResourceClass.key
+ );
+
+ let { manifestEntry, postRecoveryEntry } =
+ fakeEntryMap.get(backupResourceClass);
+
+ Assert.ok(
+ backupResourceClass.prototype.recover.calledOnce,
+ `Recover was called for ${backupResourceClass.key}`
+ );
+ Assert.ok(
+ backupResourceClass.prototype.recover.calledWith(
+ manifestEntry,
+ expectedResourceFolder,
+ newProfilePath
+ ),
+ `Recover was passed the right arguments for ${backupResourceClass.key}`
+ );
+ Assert.deepEqual(
+ postRecoveryEntry,
+ postRecovery[backupResourceClass.key],
+ "The post recovery data is as expected"
+ );
+ }
+
+ let newProfileTelemetryStateObject = await IOUtils.readJSON(
+ PathUtils.join(newProfileRootPath, "datareporting", "state.json")
+ );
+ Assert.deepEqual(
+ testTelemetryStateObject,
+ newProfileTelemetryStateObject,
+ "Recovered profile inherited telemetry state from the profile that " +
+ "initiated recovery"
+ );
+
+ await IOUtils.remove(oldProfilePath, { recursive: true });
+ await IOUtils.remove(newProfileRootPath, { recursive: true });
+ sandbox.restore();
+});
+
+/**
+ * Tests that if there's a post-recovery.json file in the profile directory
+ * when checkForPostRecovery() is called, that it is processed, and the
+ * postRecovery methods on the associated BackupResources are called with the
+ * entry values from the file.
+ */
+add_task(async function test_checkForPostRecovery() {
+ let sandbox = sinon.createSandbox();
+
+ let testProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "checkForPostRecoveryTest"
+ );
+ let fakePostRecoveryObject = {
+ [FakeBackupResource1.key]: "test 1",
+ [FakeBackupResource3.key]: "test 3",
+ };
+ await IOUtils.writeJSON(
+ PathUtils.join(testProfilePath, BackupService.POST_RECOVERY_FILE_NAME),
+ fakePostRecoveryObject
+ );
+
+ sandbox.stub(FakeBackupResource1.prototype, "postRecovery").resolves();
+ sandbox.stub(FakeBackupResource2.prototype, "postRecovery").resolves();
+ sandbox.stub(FakeBackupResource3.prototype, "postRecovery").resolves();
+
+ let bs = new BackupService({
+ FakeBackupResource1,
+ FakeBackupResource2,
+ FakeBackupResource3,
+ });
+
+ await bs.checkForPostRecovery(testProfilePath);
+ await bs.postRecoveryComplete;
+
+ Assert.ok(
+ FakeBackupResource1.prototype.postRecovery.calledOnce,
+ "FakeBackupResource1.postRecovery was called once"
+ );
+ Assert.ok(
+ FakeBackupResource2.prototype.postRecovery.notCalled,
+ "FakeBackupResource2.postRecovery was not called"
+ );
+ Assert.ok(
+ FakeBackupResource3.prototype.postRecovery.calledOnce,
+ "FakeBackupResource3.postRecovery was called once"
+ );
+ Assert.ok(
+ FakeBackupResource1.prototype.postRecovery.calledWith(
+ fakePostRecoveryObject[FakeBackupResource1.key]
+ ),
+ "FakeBackupResource1.postRecovery was called with the expected argument"
+ );
+ Assert.ok(
+ FakeBackupResource3.prototype.postRecovery.calledWith(
+ fakePostRecoveryObject[FakeBackupResource3.key]
+ ),
+ "FakeBackupResource3.postRecovery was called with the expected argument"
+ );
+
+ await IOUtils.remove(testProfilePath, { recursive: true });
+ sandbox.restore();
+});
diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_takeMeasurements.js b/browser/components/backup/tests/xpcshell/test_BackupService_takeMeasurements.js
new file mode 100644
index 0000000000..c73482dfe6
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_BackupService_takeMeasurements.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(() => {
+ // 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() {
+ let bs = new BackupService();
+ await bs.takeMeasurements();
+ let measurement = Glean.browserBackup.profDDiskSpace.testGetValue();
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent", false, true),
+ "browser.backup.prof_d_disk_space",
+ measurement
+ );
+
+ Assert.greater(
+ measurement,
+ 0,
+ "Should have collected a measurement for the profile directory storage " +
+ "device"
+ );
+});
diff --git a/browser/components/backup/tests/xpcshell/test_BrowserResource.js b/browser/components/backup/tests/xpcshell/test_BrowserResource.js
deleted file mode 100644
index 23c8e077a5..0000000000
--- a/browser/components/backup/tests/xpcshell/test_BrowserResource.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const { BackupResource } = 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.
- */
-add_task(async function test_getFileSize() {
- let file = do_get_file("data/test_xulstore.json");
-
- let testFilePath = PathUtils.join(PathUtils.profileDir, "test_xulstore.json");
-
- await IOUtils.copy(file.path, PathUtils.profileDir);
-
- let size = await BackupResource.getFileSize(testFilePath);
-
- Assert.equal(
- size,
- EXPECTED_KILOBYTES_FOR_XULSTORE,
- "Size of the test_xulstore.json is rounded up to the nearest kilobyte."
- );
-
- await IOUtils.remove(testFilePath);
-});
-
-/**
- * Tests that BackupService.getFileSize 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");
-
- // Create a test directory with the test json file in it.
- let testDir = PathUtils.join(PathUtils.profileDir, "testDir");
- await IOUtils.makeDirectory(testDir);
- await IOUtils.copy(file.path, testDir);
-
- // Create another test directory inside of that one.
- let nestedTestDir = PathUtils.join(testDir, "testDir");
- await IOUtils.makeDirectory(nestedTestDir);
- await IOUtils.copy(file.path, nestedTestDir);
-
- let size = await BackupResource.getDirectorySize(testDir);
-
- Assert.equal(
- size,
- EXPECTED_KILOBYTES_FOR_XULSTORE * 2,
- `Total size of the directory is rounded up to the nearest kilobyte
- and is equal to twice the size of the test_xulstore.json file`
- );
-
- await IOUtils.remove(testDir, { recursive: true });
-});
diff --git a/browser/components/backup/tests/xpcshell/test_CookiesBackupResource.js b/browser/components/backup/tests/xpcshell/test_CookiesBackupResource.js
new file mode 100644
index 0000000000..1690580437
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_CookiesBackupResource.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { CookiesBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/CookiesBackupResource.sys.mjs"
+);
+
+/**
+ * Tests that we can measure the Cookies db in a profile directory.
+ */
+add_task(async function test_measure() {
+ 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);
+});
+
+/**
+ * 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 cookiesBackupResource = new CookiesBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CookiesBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CookiesBackupResource-staging-test"
+ );
+
+ // Make sure this file exists in the source directory, otherwise
+ // BackupResource will skip attempting to back it up.
+ await createTestFiles(sourcePath, [{ path: "cookies.sqlite" }]);
+
+ // 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);
+
+ let manifestEntry = await cookiesBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+ Assert.equal(
+ manifestEntry,
+ null,
+ "CookiesBackupResource.backup should return null as its ManifestEntry"
+ );
+
+ // 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.calledWith(
+ PathUtils.join(stagingPath, "cookies.sqlite")
+ ),
+ "Called backup on the cookies.sqlite Sqlite connection"
+ );
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
+
+/**
+ * Test that the recover method correctly copies items from the recovery
+ * directory into the destination profile directory.
+ */
+add_task(async function test_recover() {
+ let cookiesBackupResource = new CookiesBackupResource();
+ let recoveryPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CookiesBackupResource-recovery-test"
+ );
+ let destProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CookiesBackupResource-test-profile"
+ );
+
+ const simpleCopyFiles = [{ path: "cookies.sqlite" }];
+ await createTestFiles(recoveryPath, simpleCopyFiles);
+
+ // The backup method is expected to have returned a null ManifestEntry
+ let postRecoveryEntry = await cookiesBackupResource.recover(
+ null /* manifestEntry */,
+ recoveryPath,
+ destProfilePath
+ );
+ Assert.equal(
+ postRecoveryEntry,
+ null,
+ "CookiesBackupResource.recover should return null as its post " +
+ "recovery entry"
+ );
+
+ await assertFilesExist(destProfilePath, simpleCopyFiles);
+
+ await maybeRemovePath(recoveryPath);
+ await maybeRemovePath(destProfilePath);
+});
diff --git a/browser/components/backup/tests/xpcshell/test_CredentialsAndSecurityBackupResource.js b/browser/components/backup/tests/xpcshell/test_CredentialsAndSecurityBackupResource.js
new file mode 100644
index 0000000000..f53fec8d3f
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_CredentialsAndSecurityBackupResource.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { CredentialsAndSecurityBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs"
+);
+
+/**
+ * Tests that we can measure credentials related files in the profile directory.
+ */
+add_task(async function test_measure() {
+ 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);
+});
+
+/**
+ * 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 credentialsAndSecurityBackupResource =
+ new CredentialsAndSecurityBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CredentialsAndSecurityBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CredentialsAndSecurityBackupResource-staging-test"
+ );
+
+ const simpleCopyFiles = [
+ { path: "logins.json", sizeInKB: 1 },
+ { path: "logins-backup.json", sizeInKB: 1 },
+ { path: "autofill-profiles.json", sizeInKB: 1 },
+ { path: "signedInUser.json", sizeInKB: 5 },
+ { path: "pkcs11.txt", sizeInKB: 1 },
+ ];
+ await createTestFiles(sourcePath, simpleCopyFiles);
+
+ // Create our fake database files. We don't expect these to be copied to the
+ // staging directory in this test due to our stubbing of the backup method, so
+ // we don't include it in `simpleCopyFiles`.
+ await createTestFiles(sourcePath, [
+ { path: "cert9.db" },
+ { path: "key4.db" },
+ { path: "credentialstate.sqlite" },
+ ]);
+
+ // 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);
+
+ let manifestEntry = await credentialsAndSecurityBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+
+ Assert.equal(
+ manifestEntry,
+ null,
+ "CredentialsAndSecurityBackupResource.backup should return null as its ManifestEntry"
+ );
+
+ 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.calledThrice,
+ "Called backup the expected number of times for all connections"
+ );
+ Assert.ok(
+ fakeConnection.backup.firstCall.calledWith(
+ PathUtils.join(stagingPath, "cert9.db")
+ ),
+ "Called backup on cert9.db connection first"
+ );
+ Assert.ok(
+ fakeConnection.backup.secondCall.calledWith(
+ PathUtils.join(stagingPath, "key4.db")
+ ),
+ "Called backup on key4.db connection second"
+ );
+ Assert.ok(
+ fakeConnection.backup.thirdCall.calledWith(
+ PathUtils.join(stagingPath, "credentialstate.sqlite")
+ ),
+ "Called backup on credentialstate.sqlite connection third"
+ );
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
+
+/**
+ * Test that the recover method correctly copies items from the recovery
+ * directory into the destination profile directory.
+ */
+add_task(async function test_recover() {
+ let credentialsAndSecurityBackupResource =
+ new CredentialsAndSecurityBackupResource();
+ let recoveryPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CredentialsAndSecurityBackupResource-recovery-test"
+ );
+ let destProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "CredentialsAndSecurityBackupResource-test-profile"
+ );
+
+ const files = [
+ { path: "logins.json" },
+ { path: "logins-backup.json" },
+ { path: "autofill-profiles.json" },
+ { path: "credentialstate.sqlite" },
+ { path: "signedInUser.json" },
+ { path: "cert9.db" },
+ { path: "key4.db" },
+ { path: "pkcs11.txt" },
+ ];
+ await createTestFiles(recoveryPath, files);
+
+ // The backup method is expected to have returned a null ManifestEntry
+ let postRecoveryEntry = await credentialsAndSecurityBackupResource.recover(
+ null /* manifestEntry */,
+ recoveryPath,
+ destProfilePath
+ );
+ Assert.equal(
+ postRecoveryEntry,
+ null,
+ "CredentialsAndSecurityBackupResource.recover should return null as its post " +
+ "recovery entry"
+ );
+
+ await assertFilesExist(destProfilePath, files);
+
+ await maybeRemovePath(recoveryPath);
+ await maybeRemovePath(destProfilePath);
+});
diff --git a/browser/components/backup/tests/xpcshell/test_FormHistoryBackupResource.js b/browser/components/backup/tests/xpcshell/test_FormHistoryBackupResource.js
new file mode 100644
index 0000000000..93434daa9c
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_FormHistoryBackupResource.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { FormHistoryBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/FormHistoryBackupResource.sys.mjs"
+);
+
+/**
+ * Tests that we can measure the Form History db in a profile directory.
+ */
+add_task(async function test_measure() {
+ 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);
+});
+
+/**
+ * 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 formHistoryBackupResource = new FormHistoryBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "FormHistoryBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "FormHistoryBackupResource-staging-test"
+ );
+
+ // Make sure this file exists in the source directory, otherwise
+ // BackupResource will skip attempting to back it up.
+ await createTestFiles(sourcePath, [{ path: "formhistory.sqlite" }]);
+
+ // 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);
+
+ let manifestEntry = await formHistoryBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+ Assert.equal(
+ manifestEntry,
+ null,
+ "FormHistoryBackupResource.backup should return null as its ManifestEntry"
+ );
+
+ // 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.calledWith(
+ PathUtils.join(stagingPath, "formhistory.sqlite")
+ ),
+ "Called backup on the formhistory.sqlite Sqlite connection"
+ );
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
+
+/**
+ * Test that the recover method correctly copies items from the recovery
+ * directory into the destination profile directory.
+ */
+add_task(async function test_recover() {
+ let formHistoryBackupResource = new FormHistoryBackupResource();
+ let recoveryPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "FormHistoryBackupResource-recovery-test"
+ );
+ let destProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "FormHistoryBackupResource-test-profile"
+ );
+
+ const simpleCopyFiles = [{ path: "formhistory.sqlite" }];
+ await createTestFiles(recoveryPath, simpleCopyFiles);
+
+ // The backup method is expected to have returned a null ManifestEntry
+ let postRecoveryEntry = await formHistoryBackupResource.recover(
+ null /* manifestEntry */,
+ recoveryPath,
+ destProfilePath
+ );
+ Assert.equal(
+ postRecoveryEntry,
+ null,
+ "FormHistoryBackupResource.recover should return null as its post " +
+ "recovery entry"
+ );
+
+ await assertFilesExist(destProfilePath, simpleCopyFiles);
+
+ await maybeRemovePath(recoveryPath);
+ await maybeRemovePath(destProfilePath);
+});
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..ab63b65332
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_MiscDataBackupResource.js
@@ -0,0 +1,302 @@
+/* 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"
+);
+
+const { ActivityStreamStorage } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs"
+);
+
+const { ProfileAge } = ChromeUtils.importESModule(
+ "resource://gre/modules/ProfileAge.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 = 231;
+ const tempDir = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-measurement-test"
+ );
+
+ const mockFiles = [
+ { 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: "enumerate_devices.txt" },
+ { path: "SiteSecurityServiceState.bin" },
+ ];
+ await createTestFiles(sourcePath, simpleCopyFiles);
+
+ // Create our fake database files. We don't expect this to be copied to the
+ // staging directory in this test due to our stubbing of the backup method, so
+ // we don't include it in `simpleCopyFiles`.
+ await createTestFiles(sourcePath, [{ path: "protections.sqlite" }]);
+
+ // 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);
+
+ let snippetsTableStub = {
+ getAllKeys: sandbox.stub().resolves(["key1", "key2"]),
+ get: sandbox.stub().callsFake(key => {
+ return { key: `value for ${key}` };
+ }),
+ };
+
+ sandbox
+ .stub(ActivityStreamStorage.prototype, "getDbTable")
+ .withArgs("snippets")
+ .resolves(snippetsTableStub);
+
+ let manifestEntry = await miscDataBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+ Assert.equal(
+ manifestEntry,
+ null,
+ "MiscDataBackupResource.backup should return null as its ManifestEntry"
+ );
+
+ 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 generalized ability to copy the
+ // chrome-privileged IndexedDB databases under storage/permanent/chrome, but
+ // we do support copying individual IndexedDB databases by manually exporting
+ // and re-importing their contents.
+ let snippetsBackupPath = PathUtils.join(
+ stagingPath,
+ "activity-stream-snippets.json"
+ );
+ Assert.ok(
+ await IOUtils.exists(snippetsBackupPath),
+ "The activity-stream-snippets.json file should exist"
+ );
+ let snippetsBackupContents = await IOUtils.readJSON(snippetsBackupPath);
+ Assert.deepEqual(
+ snippetsBackupContents,
+ {
+ key1: { key: "value for key1" },
+ key2: { key: "value for key2" },
+ },
+ "The contents of the activity-stream-snippets.json file should be as expected"
+ );
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
+
+/**
+ * Test that the recover method correctly copies items from the recovery
+ * directory into the destination profile directory.
+ */
+add_task(async function test_recover() {
+ let miscBackupResource = new MiscDataBackupResource();
+ let recoveryPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-recovery-test"
+ );
+ let destProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-test-profile"
+ );
+
+ // Write a dummy times.json into the xpcshell test profile directory. We
+ // expect it to be copied into the destination profile.
+ let originalProfileAge = await ProfileAge(PathUtils.profileDir);
+ await originalProfileAge.computeAndPersistCreated();
+ Assert.ok(
+ await IOUtils.exists(PathUtils.join(PathUtils.profileDir, "times.json"))
+ );
+
+ const simpleCopyFiles = [
+ { path: "enumerate_devices.txt" },
+ { path: "protections.sqlite" },
+ { path: "SiteSecurityServiceState.bin" },
+ ];
+ await createTestFiles(recoveryPath, simpleCopyFiles);
+
+ const SNIPPETS_BACKUP_FILE = "activity-stream-snippets.json";
+
+ // We'll also separately create the activity-stream-snippets.json file, which
+ // is not expected to be copied into the profile directory, but is expected
+ // to exist in the recovery path.
+ await createTestFiles(recoveryPath, [{ path: SNIPPETS_BACKUP_FILE }]);
+
+ // The backup method is expected to have returned a null ManifestEntry
+ let postRecoveryEntry = await miscBackupResource.recover(
+ null /* manifestEntry */,
+ recoveryPath,
+ destProfilePath
+ );
+ Assert.deepEqual(
+ postRecoveryEntry,
+ {
+ snippetsBackupFile: PathUtils.join(recoveryPath, SNIPPETS_BACKUP_FILE),
+ },
+ "MiscDataBackupResource.recover should return the snippets backup data " +
+ "path as its post recovery entry"
+ );
+
+ await assertFilesExist(destProfilePath, simpleCopyFiles);
+
+ // The activity-stream-snippets.json path should _not_ have been written to
+ // the profile path.
+ Assert.ok(
+ !(await IOUtils.exists(
+ PathUtils.join(destProfilePath, SNIPPETS_BACKUP_FILE)
+ )),
+ "Snippets backup data should not have gone into the profile directory"
+ );
+
+ // The times.json file should have been copied over and a backup recovery
+ // time written into it.
+ Assert.ok(
+ await IOUtils.exists(PathUtils.join(destProfilePath, "times.json"))
+ );
+ let copiedProfileAge = await ProfileAge(destProfilePath);
+ Assert.equal(
+ await originalProfileAge.created,
+ await copiedProfileAge.created,
+ "Created timestamp should match."
+ );
+ Assert.equal(
+ await originalProfileAge.firstUse,
+ await copiedProfileAge.firstUse,
+ "First use timestamp should match."
+ );
+ Assert.ok(
+ await copiedProfileAge.recoveredFromBackup,
+ "Backup recovery timestamp should have been set."
+ );
+
+ await maybeRemovePath(recoveryPath);
+ await maybeRemovePath(destProfilePath);
+});
+
+/**
+ * Test that the postRecovery method correctly writes the snippets backup data
+ * into the snippets IndexedDB table.
+ */
+add_task(async function test_postRecovery() {
+ let sandbox = sinon.createSandbox();
+
+ let fakeProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "MiscDataBackupResource-test-profile"
+ );
+ let fakeSnippetsData = {
+ key1: "value1",
+ key2: "value2",
+ };
+ const SNIPPEST_BACKUP_FILE = PathUtils.join(
+ fakeProfilePath,
+ "activity-stream-snippets.json"
+ );
+
+ await IOUtils.writeJSON(SNIPPEST_BACKUP_FILE, fakeSnippetsData);
+
+ let snippetsTableStub = {
+ set: sandbox.stub(),
+ };
+
+ sandbox
+ .stub(ActivityStreamStorage.prototype, "getDbTable")
+ .withArgs("snippets")
+ .resolves(snippetsTableStub);
+
+ let miscBackupResource = new MiscDataBackupResource();
+ await miscBackupResource.postRecovery({
+ snippetsBackupFile: SNIPPEST_BACKUP_FILE,
+ });
+
+ Assert.ok(
+ snippetsTableStub.set.calledTwice,
+ "The snippets table's set method was called twice"
+ );
+ Assert.ok(
+ snippetsTableStub.set.firstCall.calledWith("key1", "value1"),
+ "The snippets table's set method was called with the first key-value pair"
+ );
+ Assert.ok(
+ snippetsTableStub.set.secondCall.calledWith("key2", "value2"),
+ "The snippets table's set method was called with the second key-value pair"
+ );
+
+ 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..7248a5c614
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js
@@ -0,0 +1,369 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { BookmarkJSONUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/BookmarkJSONUtils.sys.mjs"
+);
+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"
+ );
+
+ // Make sure these files exist in the source directory, otherwise
+ // BackupResource will skip attempting to back them up.
+ await createTestFiles(sourcePath, [
+ { path: "places.sqlite" },
+ { path: "favicons.sqlite" },
+ ]);
+
+ let fakeConnection = {
+ backup: sandbox.stub().resolves(true),
+ close: sandbox.stub().resolves(true),
+ };
+ sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
+
+ let manifestEntry = await placesBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+ Assert.equal(
+ manifestEntry,
+ null,
+ "PlacesBackupResource.backup should return null as its ManifestEntry"
+ );
+
+ 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);
+
+ let manifestEntry = await placesBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+ Assert.deepEqual(
+ manifestEntry,
+ { bookmarksOnly: true },
+ "Should have gotten back a ManifestEntry indicating that we only copied " +
+ "bookmarks"
+ );
+
+ 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();
+ manifestEntry = await placesBackupResource.backup(stagingPath, sourcePath);
+ Assert.deepEqual(
+ manifestEntry,
+ { bookmarksOnly: true },
+ "Should have gotten back a ManifestEntry indicating that we only copied " +
+ "bookmarks"
+ );
+
+ 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);
+
+ let manifestEntry = await placesBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+ Assert.deepEqual(
+ manifestEntry,
+ { bookmarksOnly: true },
+ "Should have gotten back a ManifestEntry indicating that we only copied " +
+ "bookmarks"
+ );
+
+ 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();
+});
+
+/**
+ * Test that the recover method correctly copies places.sqlite and favicons.sqlite
+ * from the recovery directory into the destination profile directory.
+ */
+add_task(async function test_recover() {
+ let placesBackupResource = new PlacesBackupResource();
+ let recoveryPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-recovery-test"
+ );
+ let destProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-test-profile"
+ );
+
+ const simpleCopyFiles = [
+ { path: "places.sqlite" },
+ { path: "favicons.sqlite" },
+ ];
+ await createTestFiles(recoveryPath, simpleCopyFiles);
+
+ // The backup method is expected to have returned a null ManifestEntry
+ let postRecoveryEntry = await placesBackupResource.recover(
+ null /* manifestEntry */,
+ recoveryPath,
+ destProfilePath
+ );
+ Assert.equal(
+ postRecoveryEntry,
+ null,
+ "PlacesBackupResource.recover should return null as its post recovery entry"
+ );
+
+ await assertFilesExist(destProfilePath, simpleCopyFiles);
+
+ await maybeRemovePath(recoveryPath);
+ await maybeRemovePath(destProfilePath);
+});
+
+/**
+ * Test that the recover method correctly copies bookmarks.jsonlz4 from the recovery
+ * directory into the destination profile directory.
+ */
+add_task(async function test_recover_bookmarks_only() {
+ let sandbox = sinon.createSandbox();
+ let placesBackupResource = new PlacesBackupResource();
+ let recoveryPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-recovery-test"
+ );
+ let destProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PlacesBackupResource-test-profile"
+ );
+ let bookmarksImportStub = sandbox
+ .stub(BookmarkJSONUtils, "importFromFile")
+ .resolves(true);
+
+ await createTestFiles(recoveryPath, [{ path: "bookmarks.jsonlz4" }]);
+
+ // The backup method is expected to detect bookmarks import only
+ let postRecoveryEntry = await placesBackupResource.recover(
+ { bookmarksOnly: true },
+ recoveryPath,
+ destProfilePath
+ );
+
+ let expectedBookmarksPath = PathUtils.join(recoveryPath, "bookmarks.jsonlz4");
+
+ // Expect the bookmarks backup file path to be passed from recover()
+ Assert.deepEqual(
+ postRecoveryEntry,
+ { bookmarksBackupPath: expectedBookmarksPath },
+ "PlacesBackupResource.recover should return the expected post recovery entry"
+ );
+
+ // Ensure that files stored in a places backup are not copied to the new profile during recovery
+ for (let placesFile of [
+ "places.sqlite",
+ "favicons.sqlite",
+ "bookmarks.jsonlz4",
+ ]) {
+ Assert.ok(
+ !(await IOUtils.exists(PathUtils.join(destProfilePath, placesFile))),
+ `${placesFile} should not exist in the new profile`
+ );
+ }
+
+ // Now pretend that BackupService called the postRecovery method
+ await placesBackupResource.postRecovery(postRecoveryEntry);
+ Assert.ok(
+ bookmarksImportStub.calledOnce,
+ "BookmarkJSONUtils.importFromFile was called in the postRecovery step"
+ );
+
+ await maybeRemovePath(recoveryPath);
+ await maybeRemovePath(destProfilePath);
+
+ 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..2075b57e91
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_PreferencesBackupResource.js
@@ -0,0 +1,196 @@
+/* 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);
+
+ // Create our fake database files. We don't expect these to be copied to the
+ // staging directory in this test due to our stubbing of the backup method, so
+ // we don't include it in `simpleCopyFiles`.
+ await createTestFiles(sourcePath, [
+ { path: "permissions.sqlite" },
+ { path: "content-prefs.sqlite" },
+ ]);
+
+ // 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);
+
+ let manifestEntry = await preferencesBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+ Assert.equal(
+ manifestEntry,
+ null,
+ "PreferencesBackupResource.backup should return null as its ManifestEntry"
+ );
+
+ 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();
+});
+
+/**
+ * Test that the recover method correctly copies items from the recovery
+ * directory into the destination profile directory.
+ */
+add_task(async function test_recover() {
+ let preferencesBackupResource = new PreferencesBackupResource();
+ let recoveryPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PreferencesBackupResource-recovery-test"
+ );
+ let destProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "PreferencesBackupResource-test-profile"
+ );
+
+ const simpleCopyFiles = [
+ { path: "prefs.js" },
+ { path: "xulstore.json" },
+ { path: "permissions.sqlite" },
+ { path: "content-prefs.sqlite" },
+ { 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(recoveryPath, simpleCopyFiles);
+
+ // The backup method is expected to have returned a null ManifestEntry
+ let postRecoveryEntry = await preferencesBackupResource.recover(
+ null /* manifestEntry */,
+ recoveryPath,
+ destProfilePath
+ );
+ Assert.equal(
+ postRecoveryEntry,
+ null,
+ "PreferencesBackupResource.recover should return null as its post recovery entry"
+ );
+
+ await assertFilesExist(destProfilePath, simpleCopyFiles);
+
+ await maybeRemovePath(recoveryPath);
+ await maybeRemovePath(destProfilePath);
+});
diff --git a/browser/components/backup/tests/xpcshell/test_SessionStoreBackupResource.js b/browser/components/backup/tests/xpcshell/test_SessionStoreBackupResource.js
new file mode 100644
index 0000000000..d57f2d3a25
--- /dev/null
+++ b/browser/components/backup/tests/xpcshell/test_SessionStoreBackupResource.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { SessionStoreBackupResource } = ChromeUtils.importESModule(
+ "resource:///modules/backup/SessionStoreBackupResource.sys.mjs"
+);
+const { SessionStore } = ChromeUtils.importESModule(
+ "resource:///modules/sessionstore/SessionStore.sys.mjs"
+);
+
+/**
+ * Tests that we can measure the Session Store JSON and backups directory.
+ */
+add_task(async function test_measure() {
+ 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);
+});
+
+/**
+ * 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 sessionStoreBackupResource = new SessionStoreBackupResource();
+ let sourcePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "SessionStoreBackupResource-source-test"
+ );
+ let stagingPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "SessionStoreBackupResource-staging-test"
+ );
+
+ const simpleCopyFiles = [
+ { path: ["sessionstore-backups", "test-sessionstore-backup.jsonlz4"] },
+ { path: ["sessionstore-backups", "test-sessionstore-recovery.baklz4"] },
+ ];
+ await createTestFiles(sourcePath, simpleCopyFiles);
+
+ let sessionStoreState = SessionStore.getCurrentState(true);
+ let manifestEntry = await sessionStoreBackupResource.backup(
+ stagingPath,
+ sourcePath
+ );
+ Assert.equal(
+ manifestEntry,
+ null,
+ "SessionStoreBackupResource.backup should return null as its ManifestEntry"
+ );
+
+ /**
+ * We don't expect the actual file sessionstore.jsonlz4 to exist in the profile directory before calling the backup method.
+ * Instead, verify that it is created by the backup method and exists in the staging folder right after.
+ */
+ await assertFilesExist(stagingPath, [
+ ...simpleCopyFiles,
+ { path: "sessionstore.jsonlz4" },
+ ]);
+
+ /**
+ * Do a deep comparison between the recorded session state before backup and the file made in the staging folder
+ * to verify that information about session state was correctly written for backup.
+ */
+ let sessionStoreStateStaged = await IOUtils.readJSON(
+ PathUtils.join(stagingPath, "sessionstore.jsonlz4"),
+ { decompress: true }
+ );
+
+ /**
+ * These timestamps might be slightly different from one another, so we'll exclude
+ * them from the comparison.
+ */
+ delete sessionStoreStateStaged.session.lastUpdate;
+ delete sessionStoreState.session.lastUpdate;
+ Assert.deepEqual(
+ sessionStoreStateStaged,
+ sessionStoreState,
+ "sessionstore.jsonlz4 in the staging folder matches the recorded session state"
+ );
+
+ await maybeRemovePath(stagingPath);
+ await maybeRemovePath(sourcePath);
+
+ sandbox.restore();
+});
+
+/**
+ * Test that the recover method correctly copies items from the recovery
+ * directory into the destination profile directory.
+ */
+add_task(async function test_recover() {
+ let sessionStoreBackupResource = new SessionStoreBackupResource();
+ let recoveryPath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "SessionStoreBackupResource-recovery-test"
+ );
+ let destProfilePath = await IOUtils.createUniqueDirectory(
+ PathUtils.tempDir,
+ "SessionStoreBackupResource-test-profile"
+ );
+
+ const simpleCopyFiles = [
+ { path: ["sessionstore-backups", "test-sessionstore-backup.jsonlz4"] },
+ { path: ["sessionstore-backups", "test-sessionstore-recovery.baklz4"] },
+ ];
+ await createTestFiles(recoveryPath, simpleCopyFiles);
+
+ // We backup a copy of sessionstore.jsonlz4, so ensure it exists in the recovery path
+ let sessionStoreState = SessionStore.getCurrentState(true);
+ let sessionStoreBackupPath = PathUtils.join(
+ recoveryPath,
+ "sessionstore.jsonlz4"
+ );
+ await IOUtils.writeJSON(sessionStoreBackupPath, sessionStoreState, {
+ compress: true,
+ });
+
+ // The backup method is expected to have returned a null ManifestEntry
+ let postRecoveryEntry = await sessionStoreBackupResource.recover(
+ null /* manifestEntry */,
+ recoveryPath,
+ destProfilePath
+ );
+ Assert.equal(
+ postRecoveryEntry,
+ null,
+ "SessionStoreBackupResource.recover should return null as its post recovery entry"
+ );
+
+ await assertFilesExist(destProfilePath, [
+ ...simpleCopyFiles,
+ { path: "sessionstore.jsonlz4" },
+ ]);
+
+ let sessionStateCopied = await IOUtils.readJSON(
+ PathUtils.join(destProfilePath, "sessionstore.jsonlz4"),
+ { decompress: true }
+ );
+
+ /**
+ * These timestamps might be slightly different from one another, so we'll exclude
+ * them from the comparison.
+ */
+ delete sessionStateCopied.session.lastUpdate;
+ delete sessionStoreState.session.lastUpdate;
+ Assert.deepEqual(
+ sessionStateCopied,
+ sessionStoreState,
+ "sessionstore.jsonlz4 in the destination profile folder matches the backed up session state"
+ );
+
+ await maybeRemovePath(recoveryPath);
+ await maybeRemovePath(destProfilePath);
+});
diff --git a/browser/components/backup/tests/xpcshell/test_measurements.js b/browser/components/backup/tests/xpcshell/test_measurements.js
deleted file mode 100644
index e5726126b2..0000000000
--- a/browser/components/backup/tests/xpcshell/test_measurements.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-const { BackupService } = ChromeUtils.importESModule(
- "resource:///modules/backup/BackupService.sys.mjs"
-);
-
-const { TelemetryTestUtils } = ChromeUtils.importESModule(
- "resource://testing-common/TelemetryTestUtils.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 we can measure the disk space available in the profile directory.
- */
-add_task(async function test_profDDiskSpace() {
- let bs = new BackupService();
- await bs.takeMeasurements();
- let measurement = Glean.browserBackup.profDDiskSpace.testGetValue();
- TelemetryTestUtils.assertScalar(
- TelemetryTestUtils.getProcessScalars("parent", false, true),
- "browser.backup.prof_d_disk_space",
- measurement
- );
-
- Assert.greater(
- measurement,
- 0,
- "Should have collected a measurement for the profile directory storage " +
- "device"
- );
-});
diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml
index fb6dcd6846..8a41c9e761 100644
--- a/browser/components/backup/tests/xpcshell/xpcshell.toml
+++ b/browser/components/backup/tests/xpcshell/xpcshell.toml
@@ -1,8 +1,30 @@
[DEFAULT]
+head = "head.js"
firefox-appdir = "browser"
skip-if = ["os == 'android'"]
+prefs = [
+ "browser.backup.log=true",
+]
-["test_BrowserResource.js"]
+["test_AddonsBackupResource.js"]
+
+["test_BackupResource.js"]
support-files = ["data/test_xulstore.json"]
-["test_measurements.js"]
+["test_BackupService.js"]
+
+["test_BackupService_takeMeasurements.js"]
+
+["test_CookiesBackupResource.js"]
+
+["test_CredentialsAndSecurityBackupResource.js"]
+
+["test_FormHistoryBackupResource.js"]
+
+["test_MiscDataBackupResource.js"]
+
+["test_PlacesBackupResource.js"]
+
+["test_PreferencesBackupResource.js"]
+
+["test_SessionStoreBackupResource.js"]
diff --git a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs
index 34f132a539..901059906a 100644
--- a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs
+++ b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs
@@ -25,6 +25,7 @@ XPCOMUtils.defineLazyServiceGetter(
ChromeUtils.defineESModuleGetters(lazy, {
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
@@ -42,6 +43,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
@@ -204,7 +212,7 @@ export const ContentAnalysis = {
* Registers for various messages/events that will indicate the
* need for communicating something to the user.
*/
- initialize() {
+ initialize(doc) {
if (!this.isInitialized) {
this.isInitialized = true;
this.initializeDownloadCA();
@@ -216,6 +224,17 @@ export const ContentAnalysis = {
);
});
}
+
+ // Do this even if initialized so the icon shows up on new windows, not just the
+ // first one.
+ if (lazy.gContentAnalysis.isActive) {
+ doc.l10n.setAttributes(
+ doc.getElementById("content-analysis-indicator"),
+ "content-analysis-indicator-tooltip",
+ { agentName: lazy.agentName }
+ );
+ doc.documentElement.setAttribute("contentanalysisactive", "true");
+ }
},
async uninitialize() {
@@ -236,7 +255,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 +312,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 +345,7 @@ export const ContentAnalysis = {
timer: lazy.setTimeout(() => {
this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, {
notification: this._showSlowCAMessage(
- operation,
+ analysisType,
request,
resourceNameOrOperationType,
browsingContext
@@ -338,7 +357,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,15 +391,29 @@ export const ContentAnalysis = {
windowAndResourceNameOrOperationType.resourceNameOrOperationType,
windowAndResourceNameOrOperationType.browsingContext,
request.requestToken,
- responseResult
+ responseResult,
+ request.cancelError
);
this._showAnotherPendingDialog(
windowAndResourceNameOrOperationType.browsingContext
);
break;
+ }
}
},
+ async showPanel(element, panelUI) {
+ element.ownerDocument.l10n.setAttributes(
+ lazy.PanelMultiView.getViewNode(
+ element.ownerDocument,
+ "content-analysis-panel-description"
+ ),
+ "content-analysis-panel-text",
+ { agentName: lazy.agentName }
+ );
+ panelUI.showSubView("content-analysis-panel", element);
+ },
+
_showAnotherPendingDialog(aBrowsingContext) {
const otherBrowsingContext =
this.dlpBusyViewsByTopBrowsingContext.getBrowsingContextWithPendingNotification(
@@ -441,7 +474,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 +496,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 +515,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 +626,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 "";
}
@@ -599,6 +642,36 @@ export const ContentAnalysis = {
});
},
+ _getErrorDialogMessage(aResourceNameOrOperationType) {
+ if (aResourceNameOrOperationType.name) {
+ return this.l10n.formatValueSync(
+ "contentanalysis-error-message-upload-file",
+ {
+ filename: aResourceNameOrOperationType.name,
+ }
+ );
+ }
+ let l10nId = undefined;
+ switch (aResourceNameOrOperationType.operationType) {
+ case Ci.nsIContentAnalysisRequest.eClipboard:
+ l10nId = "contentanalysis-error-message-clipboard";
+ break;
+ case Ci.nsIContentAnalysisRequest.eDroppedText:
+ l10nId = "contentanalysis-error-message-dropped-text";
+ break;
+ case Ci.nsIContentAnalysisRequest.eOperationPrint:
+ l10nId = "contentanalysis-error-message-print";
+ break;
+ }
+ if (!l10nId) {
+ console.error(
+ "Unknown operationTypeForDisplay: ",
+ aResourceNameOrOperationType
+ );
+ return "";
+ }
+ return this.l10n.formatValueSync(l10nId);
+ },
_showSlowCABlockingMessage(
aBrowsingContext,
aRequestToken,
@@ -655,7 +728,8 @@ export const ContentAnalysis = {
aResourceNameOrOperationType,
aBrowsingContext,
aRequestToken,
- aCAResult
+ aCAResult,
+ aRequestCancelError
) {
let message = null;
let timeoutMs = 0;
@@ -676,7 +750,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,
@@ -690,7 +764,7 @@ export const ContentAnalysis = {
Ci.nsIPromptService.BUTTON_TITLE_IS_STRING +
Ci.nsIPromptService.BUTTON_POS_1 *
Ci.nsIPromptService.BUTTON_TITLE_IS_STRING +
- Ci.nsIPromptService.BUTTON_POS_1_DEFAULT,
+ Ci.nsIPromptService.BUTTON_POS_2_DEFAULT,
await this.l10n.formatValue(
"contentanalysis-warndialog-response-allow"
),
@@ -704,22 +778,98 @@ export const ContentAnalysis = {
const allow = result.get("buttonNumClicked") === 0;
lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow);
return null;
- case Ci.nsIContentAnalysisResponse.eBlock:
- message = await this.l10n.formatValue("contentanalysis-block-message", {
- content: this._getResourceNameFromNameOrOperationType(
- aResourceNameOrOperationType
- ),
- });
- timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
- break;
+ }
+ case Ci.nsIContentAnalysisResponse.eBlock: {
+ if (!lazy.showBlockedResult) {
+ // Don't show anything
+ return null;
+ }
+ let titleId = undefined;
+ let body = undefined;
+ if (aResourceNameOrOperationType.name) {
+ titleId = "contentanalysis-block-dialog-title-upload-file";
+ body = this.l10n.formatValueSync(
+ "contentanalysis-block-dialog-body-upload-file",
+ {
+ filename: aResourceNameOrOperationType.name,
+ }
+ );
+ } else {
+ let bodyId = undefined;
+ switch (aResourceNameOrOperationType.operationType) {
+ case Ci.nsIContentAnalysisRequest.eClipboard:
+ titleId = "contentanalysis-block-dialog-title-clipboard";
+ bodyId = "contentanalysis-block-dialog-body-clipboard";
+ break;
+ case Ci.nsIContentAnalysisRequest.eDroppedText:
+ titleId = "contentanalysis-block-dialog-title-dropped-text";
+ bodyId = "contentanalysis-block-dialog-body-dropped-text";
+ break;
+ case Ci.nsIContentAnalysisRequest.eOperationPrint:
+ titleId = "contentanalysis-block-dialog-title-print";
+ bodyId = "contentanalysis-block-dialog-body-print";
+ break;
+ }
+ if (!titleId || !bodyId) {
+ console.error(
+ "Unknown operationTypeForDisplay: ",
+ aResourceNameOrOperationType
+ );
+ return null;
+ }
+ body = this.l10n.formatValueSync(bodyId);
+ }
+ Services.prompt.alertBC(
+ aBrowsingContext,
+ Ci.nsIPromptService.MODAL_TYPE_TAB,
+ this.l10n.formatValueSync(titleId),
+ body
+ );
+ return null;
+ }
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-content",
+ {
+ agent: lazy.agentName,
+ content: this._getErrorDialogMessage(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-content";
+ break;
+ case Ci.nsIContentAnalysisResponse.eInvalidAgentSignature:
+ messageId =
+ "contentanalysis-invalid-agent-signature-message-content";
+ break;
+ case Ci.nsIContentAnalysisResponse.eErrorOther:
+ messageId = "contentanalysis-unspecified-error-message-content";
+ break;
+ default:
+ console.error(
+ "Unexpected CA cancelError value: " + aRequestCancelError
+ );
+ messageId = "contentanalysis-unspecified-error-message-content";
+ break;
+ }
+ message = await this.l10n.formatValue(messageId, {
+ agent: lazy.agentName,
+ content: this._getErrorDialogMessage(aResourceNameOrOperationType),
+ });
+ timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
+ }
+ break;
default:
throw new Error("Unexpected CA result value: " + aCAResult);
}
diff --git a/browser/components/contextualidentity/content/usercontext.css b/browser/components/contextualidentity/content/usercontext.css
index f12625c08f..52e65ade1f 100644
--- a/browser/components/contextualidentity/content/usercontext.css
+++ b/browser/components/contextualidentity/content/usercontext.css
@@ -106,8 +106,12 @@
}
#userContext-label {
- margin: 0;
- color: var(--identity-tab-color);
+ color: var(--identity-tab-color);
+ margin: 0;
+ max-width: 8em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
#userContext-icons {
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..09b57be4c9 100644
--- a/browser/components/customizableui/CustomizableWidgets.sys.mjs
+++ b/browser/components/customizableui/CustomizableWidgets.sys.mjs
@@ -99,6 +99,21 @@ export const CustomizableWidgets = [
case "unload":
this.onWindowUnload(event);
break;
+ case "command": {
+ let { target } = event;
+ let { PanelUI, PlacesCommandHook } = target.ownerGlobal;
+ if (target.id == "appMenuRecentlyClosedTabs") {
+ PanelUI.showSubView(this.recentlyClosedTabsPanel, target);
+ } else if (target.id == "appMenuRecentlyClosedWindows") {
+ PanelUI.showSubView(this.recentlyClosedWindowsPanel, target);
+ } else if (target.id == "appMenuSearchHistory") {
+ PlacesCommandHook.searchHistory();
+ } else if (target.id == "PanelUI-historyMore") {
+ PlacesCommandHook.showPlacesOrganizer("History");
+ lazy.CustomizableUI.hidePanelForNode(target);
+ }
+ break;
+ }
default:
throw new Error(`Unsupported event for '${this.id}'`);
}
@@ -153,9 +168,10 @@ export const CustomizableWidgets = [
// When the popup is hidden (thus the panelmultiview node as well), make
// sure to stop listening to PlacesDatabase updates.
panelview.panelMultiView.addEventListener("PanelMultiViewHidden", this);
+ panelview.addEventListener("command", this);
window.addEventListener("unload", this);
},
- onViewHiding(event) {
+ onViewHiding() {
lazy.log.debug("History view is being hidden!");
},
onPanelMultiViewHidden(event) {
@@ -172,10 +188,14 @@ export const CustomizableWidgets = [
document,
this.recentlyClosedWindowsPanel
).removeEventListener("ViewShowing", this);
+ lazy.PanelMultiView.getViewNode(
+ document,
+ this.viewId
+ ).removeEventListener("command", this);
}
panelMultiView.removeEventListener("PanelMultiViewHidden", this);
},
- onWindowUnload(event) {
+ onWindowUnload() {
if (this._panelMenuView) {
delete this._panelMenuView;
}
@@ -260,7 +280,7 @@ export const CustomizableWidgets = [
tooltiptext: "sidebar-button.tooltiptext2",
onCommand(aEvent) {
let win = aEvent.target.ownerGlobal;
- win.SidebarUI.toggle();
+ win.SidebarController.toggle();
},
onCreated(aNode) {
// Add an observer so the button is checked while the sidebar is open
@@ -419,7 +439,7 @@ export const CustomizableWidgets = [
id: "characterencoding-button",
l10nId: "repair-text-encoding-button",
onCommand(aEvent) {
- aEvent.view.BrowserForceEncodingDetection();
+ aEvent.view.BrowserCommands.forceEncodingDetection();
},
},
{
@@ -464,10 +484,52 @@ if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
lazy.PanelMultiView.getViewNode(doc, "PanelUI-remotetabs-deck"),
lazy.PanelMultiView.getViewNode(doc, "PanelUI-remotetabs-tabslist")
);
+ panelview.addEventListener("command", this);
+ let syncNowButton = lazy.PanelMultiView.getViewNode(
+ aEvent.target.ownerDocument,
+ "PanelUI-remotetabs-syncnow"
+ );
+ syncNowButton.addEventListener("mouseover", this);
},
onViewHiding(aEvent) {
- aEvent.target.syncedTabsPanelList.destroy();
- aEvent.target.syncedTabsPanelList = null;
+ let panelview = aEvent.target;
+ panelview.syncedTabsPanelList.destroy();
+ panelview.syncedTabsPanelList = null;
+ panelview.removeEventListener("command", this);
+ let syncNowButton = lazy.PanelMultiView.getViewNode(
+ aEvent.target.ownerDocument,
+ "PanelUI-remotetabs-syncnow"
+ );
+ syncNowButton.removeEventListener("mouseover", this);
+ },
+ handleEvent(aEvent) {
+ let button = aEvent.target;
+ let { gSync } = button.ownerGlobal;
+ switch (aEvent.type) {
+ case "mouseover":
+ gSync.refreshSyncButtonsTooltip();
+ break;
+ case "command": {
+ switch (button.id) {
+ case "PanelUI-remotetabs-syncnow":
+ gSync.doSync();
+ break;
+ case "PanelUI-remotetabs-view-managedevices":
+ gSync.openDevicesManagementPage("syncedtabs-menupanel");
+ break;
+ case "PanelUI-remotetabs-tabsdisabledpane-button":
+ case "PanelUI-remotetabs-setupsync-button":
+ case "PanelUI-remotetabs-syncdisabled-button":
+ case "PanelUI-remotetabs-reauthsync-button":
+ case "PanelUI-remotetabs-unverified-button":
+ gSync.openPrefs("synced-tabs");
+ break;
+ case "PanelUI-remotetabs-connect-device-button":
+ gSync.openConnectAnotherDevice("synced-tabs");
+ break;
+ }
+ }
+ }
},
});
}
diff --git a/browser/components/customizableui/CustomizeMode.sys.mjs b/browser/components/customizableui/CustomizeMode.sys.mjs
index 5f6d01d833..41f347130e 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();
},
@@ -1378,7 +1376,7 @@ CustomizeMode.prototype = {
},
openAddonsManagerThemes() {
- this.window.BrowserOpenAddonsMgr("addons://list/theme");
+ this.window.BrowserAddonUI.openAddonsMgr("addons://list/theme");
},
getMoreThemes(aEvent) {
@@ -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.inc.xhtml b/browser/components/customizableui/content/panelUI.inc.xhtml
index 956a6ae45d..7607f59ec4 100644
--- a/browser/components/customizableui/content/panelUI.inc.xhtml
+++ b/browser/components/customizableui/content/panelUI.inc.xhtml
@@ -107,7 +107,7 @@
<toolbarbutton id="unified-extensions-manage-extensions"
class="subviewbutton panel-subview-footer-button unified-extensions-manage-extensions"
data-l10n-id="unified-extensions-manage-extensions"
- oncommand="BrowserOpenAddonsMgr('addons://list/extension');" />
+ oncommand="BrowserAddonUI.openAddonsMgr('addons://list/extension');" />
</panelview>
</panelmultiview>
</panel>
@@ -271,7 +271,7 @@
flip="slide"
position="bottomright topright"
noautofocus="true">
- <panelmultiview id="appMenu-multiView" mainViewId="appMenu-protonMainView"
+ <panelmultiview id="appMenu-multiView" mainViewId="appMenu-mainView"
viewCacheId="appMenu-viewCache">
</panelmultiview>
</panel>
diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js
index f99560bd42..cbd27e465e 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",
});
/**
@@ -129,10 +128,16 @@ const PanelUI = {
this.panel.addEventListener(event, this);
}
+ this._onLibraryCommand = this._onLibraryCommand.bind(this);
PanelMultiView.getViewNode(document, "PanelUI-helpView").addEventListener(
"ViewShowing",
this._onHelpViewShow
);
+ PanelMultiView.getViewNode(
+ document,
+ "appMenu-libraryView"
+ ).addEventListener("command", this._onLibraryCommand);
+ this.mainView.addEventListener("command", this);
this._eventListenersAdded = true;
},
@@ -144,6 +149,11 @@ const PanelUI = {
document,
"PanelUI-helpView"
).removeEventListener("ViewShowing", this._onHelpViewShow);
+ PanelMultiView.getViewNode(
+ document,
+ "appMenu-libraryView"
+ ).removeEventListener("command", this._onLibraryCommand);
+ this.mainView.removeEventListener("command", this);
this._eventListenersAdded = false;
},
@@ -167,9 +177,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,10 +310,53 @@ const PanelUI = {
case "activate":
this.updateNotifications();
break;
- case "ViewShowing":
- if (aEvent.target == this.whatsNewPanel) {
- this.onWhatsNewPanelShowing();
- }
+ case "command":
+ this.onCommand(aEvent);
+ break;
+ }
+ },
+
+ // Note that we listen for bubbling command events. In the case where the
+ // button that the user clicks has a command attribute, those events are
+ // redirected to the relevant command element, and we never see them in
+ // here. Bear this in mind if you want to write code that applies to
+ // all commands, for which this wouldn't work well.
+ onCommand(aEvent) {
+ let { target } = aEvent;
+ switch (target.id) {
+ case "appMenu-update-banner":
+ this._onBannerItemSelected(aEvent);
+ break;
+ case "appMenu-fxa-label2":
+ gSync.toggleAccountPanel(target, aEvent);
+ break;
+ case "appMenu-profiles-button":
+ gProfiles.updateView(target);
+ break;
+ case "appMenu-bookmarks-button":
+ BookmarkingUI.showSubView(target);
+ break;
+ case "appMenu-history-button":
+ this.showSubView("PanelUI-history", target);
+ break;
+ case "appMenu-passwords-button":
+ LoginHelper.openPasswordManager(window, { entryPoint: "mainmenu" });
+ break;
+ case "appMenu-fullscreen-button2":
+ // Note that we're custom-handling the hiding of the panel to make
+ // sure it disappears before entering fullscreen. Otherwise it can
+ // end up moving around on the screen during the fullscreen transition.
+ target.closest("panel").hidePopup();
+ setTimeout(() => BrowserCommands.fullScreen(), 0);
+ break;
+ case "appMenu-settings-button":
+ openPreferences();
+ break;
+ case "appMenu-more-button2":
+ this.showMoreToolsPanel(target);
+ break;
+ case "appMenu-help-button2":
+ this.showSubView("PanelUI-helpView", target);
break;
}
},
@@ -412,7 +462,6 @@ const PanelUI = {
return;
}
- this.ensureWhatsNewInitialized(viewNode);
this.ensurePanicViewInitialized(viewNode);
let container = aAnchor.closest("panelmultiview");
@@ -497,24 +546,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 +564,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 +588,7 @@ const PanelUI = {
}
},
- onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) {
+ onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
if (aContainer == this.overflowFixedList) {
this.updateOverflowStatus();
}
@@ -601,7 +621,7 @@ const PanelUI = {
}
},
- _onHelpViewShow(aEvent) {
+ _onHelpViewShow() {
// Call global menu setup function
buildHelpMenu();
@@ -671,6 +691,22 @@ const PanelUI = {
items.appendChild(fragment);
},
+ _onLibraryCommand(aEvent) {
+ let button = aEvent.target;
+ let { BookmarkingUI, DownloadsPanel } = button.ownerGlobal;
+ switch (button.id) {
+ case "appMenu-library-bookmarks-button":
+ BookmarkingUI.showSubView(button);
+ break;
+ case "appMenu-library-history-button":
+ this.showSubView("PanelUI-history", button);
+ break;
+ case "appMenu-library-downloads-button":
+ DownloadsPanel.showDownloadsHistory();
+ break;
+ }
+ },
+
_hidePopup() {
if (!this._notificationPanel) {
return;
@@ -879,10 +915,7 @@ const PanelUI = {
get mainView() {
if (!this._mainView) {
- this._mainView = PanelMultiView.getViewNode(
- document,
- "appMenu-protonMainView"
- );
+ this._mainView = PanelMultiView.getViewNode(document, "appMenu-mainView");
}
return this._mainView;
},
@@ -891,7 +924,7 @@ const PanelUI = {
if (!this._addonNotificationContainer) {
this._addonNotificationContainer = PanelMultiView.getViewNode(
document,
- "appMenu-proton-addon-banners"
+ "appMenu-addon-banners"
);
}
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_PanelMultiView_keyboard.js b/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js
index b41fc2ef23..d8c687d88a 100644
--- a/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js
+++ b/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js
@@ -150,7 +150,6 @@ add_setup(async function () {
gLink.innerText = "gLink";
gLink.id = "gLink";
gMainView.appendChild(gLink);
- await window.ensureCustomElements("moz-toggle");
gToggle = document.createElement("moz-toggle");
gToggle.label = "Test label";
gMainView.appendChild(gToggle);
diff --git a/browser/components/customizableui/test/browser_ctrl_click_panel_opening.js b/browser/components/customizableui/test/browser_ctrl_click_panel_opening.js
index 9377c28950..92528f2537 100644
--- a/browser/components/customizableui/test/browser_ctrl_click_panel_opening.js
+++ b/browser/components/customizableui/test/browser_ctrl_click_panel_opening.js
@@ -13,8 +13,8 @@ add_task(async function test_appMenu_mainView() {
return;
}
- let mainViewID = "appMenu-protonMainView";
- const mainView = document.getElementById(mainViewID);
+ let mainViewID = "appMenu-mainView";
+ const mainView = PanelMultiView.getViewNode(document, mainViewID);
let shownPromise = BrowserTestUtils.waitForEvent(mainView, "ViewShown");
// Should still open the panel when Ctrl key is pressed.
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_bannerVisibility.js b/browser/components/customizableui/test/browser_panelUINotifications_bannerVisibility.js
index df856dd4cf..686d600601 100644
--- a/browser/components/customizableui/test/browser_panelUINotifications_bannerVisibility.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications_bannerVisibility.js
@@ -24,7 +24,7 @@ add_task(async function testBannerVisibilityBeforeOpen() {
menuButton.click();
await shown;
- let banner = newWin.document.getElementById("appMenu-proton-update-banner");
+ let banner = newWin.document.getElementById("appMenu-update-banner");
let labelPromise = BrowserTestUtils.waitForMutationCondition(
banner,
@@ -62,7 +62,7 @@ add_task(async function testBannerVisibilityDuringOpen() {
menuButton.click();
await shown;
- let banner = newWin.document.getElementById("appMenu-proton-update-banner");
+ let banner = newWin.document.getElementById("appMenu-update-banner");
ok(
!banner.hasAttribute("label"),
"Update banner shouldn't contain text before notification"
@@ -109,7 +109,7 @@ add_task(async function testBannerVisibilityAfterClose() {
ok(newWin.PanelUI.mainView.hasAttribute("visible"));
- let banner = newWin.document.getElementById("appMenu-proton-update-banner");
+ let banner = newWin.document.getElementById("appMenu-update-banner");
ok(banner.hidden, "Update banner should be hidden before notification");
ok(
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_sidebar_toggle.js b/browser/components/customizableui/test/browser_sidebar_toggle.js
index 5742f368ee..a063cc26cf 100644
--- a/browser/components/customizableui/test/browser_sidebar_toggle.js
+++ b/browser/components/customizableui/test/browser_sidebar_toggle.js
@@ -9,7 +9,7 @@ registerCleanupFunction(async function () {
// Ensure sidebar is hidden after each test:
if (!document.getElementById("sidebar-box").hidden) {
- SidebarUI.hide();
+ SidebarController.hide();
}
});
@@ -21,14 +21,14 @@ var showSidebar = async function (win = window) {
);
EventUtils.synthesizeMouseAtCenter(button, {}, win);
await sidebarFocusedPromise;
- ok(win.SidebarUI.isOpen, "Sidebar is opened");
+ ok(win.SidebarController.isOpen, "Sidebar is opened");
ok(button.hasAttribute("checked"), "Toolbar button is checked");
};
var hideSidebar = async function (win = window) {
let button = win.document.getElementById("sidebar-button");
EventUtils.synthesizeMouseAtCenter(button, {}, win);
- ok(!win.SidebarUI.isOpen, "Sidebar is closed");
+ ok(!win.SidebarController.isOpen, "Sidebar is closed");
ok(!button.hasAttribute("checked"), "Toolbar button isn't checked");
};
@@ -37,18 +37,26 @@ add_task(async function () {
CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
await showSidebar();
- is(SidebarUI.currentID, "viewBookmarksSidebar", "Default sidebar selected");
- await SidebarUI.show("viewHistorySidebar");
+ is(
+ SidebarController.currentID,
+ "viewBookmarksSidebar",
+ "Default sidebar selected"
+ );
+ await SidebarController.show("viewHistorySidebar");
await hideSidebar();
await showSidebar();
- is(SidebarUI.currentID, "viewHistorySidebar", "Selected sidebar remembered");
+ is(
+ SidebarController.currentID,
+ "viewHistorySidebar",
+ "Selected sidebar remembered"
+ );
await hideSidebar();
let otherWin = await BrowserTestUtils.openNewBrowserWindow();
await showSidebar(otherWin);
is(
- otherWin.SidebarUI.currentID,
+ otherWin.SidebarController.currentID,
"viewHistorySidebar",
"Selected sidebar remembered across windows"
);
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..c99223a80e 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() {
@@ -216,6 +216,17 @@ add_task(async function () {
ok(button, "found the button");
await document.getElementById("nav-bar").overflowable.show();
+ // Actually show the fxa view:
+ let shown = BrowserTestUtils.waitForEvent(
+ document.getElementById("PanelUI-remotetabs"),
+ "ViewShown"
+ );
+ PanelUI.showSubView(
+ "PanelUI-remotetabs",
+ document.getElementById("sync-button")
+ );
+ await shown;
+
let expectedUrl =
"https://example.com/connect_another_device?context=" +
"fx_desktop_v3&entrypoint=synced-tabs&service=sync&uid=uid&email=foo%40bar.com";
@@ -325,20 +336,28 @@ add_task(async function () {
node = node.firstElementChild;
is(node.getAttribute("itemtype"), "client", "node is a client entry");
is(node.textContent, "My Desktop", "correct client");
- // Next entry is the most-recent tab
+ // Next node is an hbox, that contains the tab and potentially
+ // a button for closing the tab remotely
node = node.nextElementSibling;
- is(node.getAttribute("itemtype"), "tab", "node is a tab");
- is(node.getAttribute("label"), "http://example.com/10");
+ is(node.nodeName, "hbox");
+ // Next entry is the most-recent tab
+ let childNode = node.firstElementChild;
+ is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
+ is(childNode.getAttribute("label"), "http://example.com/10");
// Next entry is the next-most-recent tab
node = node.nextElementSibling;
- is(node.getAttribute("itemtype"), "tab", "node is a tab");
- is(node.getAttribute("label"), "http://example.com/5");
+ is(node.nodeName, "hbox");
+ childNode = node.firstElementChild;
+ is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
+ is(childNode.getAttribute("label"), "http://example.com/5");
// Next entry is the least-recent tab from the first client.
node = node.nextElementSibling;
- is(node.getAttribute("itemtype"), "tab", "node is a tab");
- is(node.getAttribute("label"), "http://example.com/1");
+ is(node.nodeName, "hbox");
+ childNode = node.firstElementChild;
+ is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
+ is(childNode.getAttribute("label"), "http://example.com/1");
node = node.nextElementSibling;
is(node, null, "no more siblings");
@@ -357,8 +376,10 @@ add_task(async function () {
is(node.textContent, "My Other Desktop", "correct client");
// Its single tab
node = node.nextElementSibling;
- is(node.getAttribute("itemtype"), "tab", "node is a tab");
- is(node.getAttribute("label"), "http://example.com/6");
+ is(node.nodeName, "hbox");
+ childNode = node.firstElementChild;
+ is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
+ is(childNode.getAttribute("label"), "http://example.com/6");
node = node.nextElementSibling;
is(node, null, "no more siblings");
@@ -378,7 +399,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");
@@ -468,14 +489,16 @@ add_task(async function () {
is(node.textContent, "My Desktop", "correct client");
for (let i = 0; i < tabsShownCount; i++) {
node = node.nextElementSibling;
- is(node.getAttribute("itemtype"), "tab", "node is a tab");
+ is(node.nodeName, "hbox");
+ let childNode = node.firstElementChild;
+ is(childNode.getAttribute("itemtype"), "tab", "node is a tab");
is(
- node.getAttribute("label"),
+ childNode.getAttribute("label"),
"Tab #" + (i + 1),
"the tab is the correct one"
);
is(
- node.getAttribute("targetURI"),
+ childNode.getAttribute("targetURI"),
SAMPLE_TAB_URL,
"url is the correct one"
);
@@ -498,7 +521,9 @@ add_task(async function () {
async function checkCanOpenURL() {
let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
- let node = tabList.firstElementChild.firstElementChild.nextElementSibling;
+ let node =
+ tabList.firstElementChild.firstElementChild.nextElementSibling
+ .firstElementChild;
let promiseTabOpened = BrowserTestUtils.waitForLocationChange(
gBrowser,
SAMPLE_TAB_URL
@@ -514,7 +539,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/distribution.sys.mjs b/browser/components/distribution.sys.mjs
index 369de15ab2..f58fd7a419 100644
--- a/browser/components/distribution.sys.mjs
+++ b/browser/components/distribution.sys.mjs
@@ -247,21 +247,10 @@ DistributionCustomizer.prototype = {
if (item.icon && item.iconData) {
try {
- let faviconURI = Services.io.newURI(item.icon);
- lazy.PlacesUtils.favicons.replaceFaviconDataFromDataURL(
- faviconURI,
- item.iconData,
- 0,
- Services.scriptSecurityManager.getSystemPrincipal()
- );
-
- lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
+ lazy.PlacesUtils.favicons.setFaviconForPage(
Services.io.newURI(item.link),
- faviconURI,
- false,
- lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
- null,
- Services.scriptSecurityManager.getSystemPrincipal()
+ Services.io.newURI(item.icon),
+ Services.io.newURI(item.iconData)
);
} catch (e) {
console.error(e);
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/downloads/content/allDownloadsView.js b/browser/components/downloads/content/allDownloadsView.js
index 08f8bfcb5f..f880f602aa 100644
--- a/browser/components/downloads/content/allDownloadsView.js
+++ b/browser/components/downloads/content/allDownloadsView.js
@@ -141,7 +141,7 @@ HistoryDownloadElementShell.prototype = {
// be opened.
let browserWin = BrowserWindowTracker.getTopWindow();
let openWhere = browserWin
- ? browserWin.whereToOpenLink(event, false, true)
+ ? BrowserUtils.whereToOpenLink(event, false, true)
: "window";
if (["window", "tabshifted", "tab"].includes(openWhere)) {
command += ":" + openWhere;
diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js
index b420d48db6..cab8b65aab 100644
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -97,8 +97,6 @@ var DownloadsPanel = {
window.addEventListener("unload", this.onWindowUnload);
- window.ensureCustomElements("moz-button-group");
-
// Load and resume active downloads if required. If there are downloads to
// be shown in the panel, they will be loaded asynchronously.
DownloadsCommon.initializeAllDataLinks();
@@ -327,7 +325,7 @@ var DownloadsPanel = {
// to the browser window when the panel closes automatically.
this.hidePanel();
- BrowserDownloadsUI();
+ BrowserCommands.downloadsUI();
},
// Internal functions
@@ -868,7 +866,7 @@ var DownloadsView = {
} else if (aEvent.shiftKey || aEvent.ctrlKey || aEvent.metaKey) {
// We adjust the command for supported modifiers to suggest where the download
// may be opened
- let openWhere = target.ownerGlobal.whereToOpenLink(aEvent, false, true);
+ let openWhere = BrowserUtils.whereToOpenLink(aEvent, false, true);
if (["tab", "window", "tabshifted"].includes(openWhere)) {
command += ":" + openWhere;
}
diff --git a/browser/components/enterprisepolicies/Policies.sys.mjs b/browser/components/enterprisepolicies/Policies.sys.mjs
index 41fc89957c..ebc1bc0a58 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,10 +540,35 @@ 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
+ );
+ }
+ if ("DefaultResult" in param) {
+ if (
+ !Number.isInteger(param.DefaultResult) ||
+ param.DefaultResult < 0 ||
+ param.DefaultResult > 2
+ ) {
+ lazy.log.error(
+ `Non-integer or out of range value for DefaultResult: ${param.DefaultResult}`
+ );
+ } else {
+ setAndLockPref(
+ "browser.contentanalysis.default_result",
+ param.DefaultResult
+ );
+ }
+ }
let boolPrefs = [
["IsPerUser", "is_per_user"],
["ShowBlockedResult", "show_blocked_result"],
- ["DefaultAllow", "default_allow"],
+ ["BypassForSameTabOperations", "bypass_for_same_tab_operations"],
];
for (let pref of boolPrefs) {
if (pref[0] in param) {
@@ -764,6 +789,15 @@ export var Policies = {
},
},
+ DisableEncryptedClientHello: {
+ onBeforeAddons(manager, param) {
+ if (param) {
+ setAndLockPref("network.dns.echconfig.enabled", false);
+ setAndLockPref("network.dns.http3_echconfig.enabled", false);
+ }
+ },
+ },
+
DisableFeedbackCommands: {
onBeforeUIStartup(manager, param) {
if (param) {
@@ -1506,6 +1540,31 @@ export var Policies = {
},
},
+ HttpAllowlist: {
+ onBeforeAddons(manager, param) {
+ addAllowDenyPermissions("https-only-load-insecure", param);
+ },
+ },
+
+ HttpsOnlyMode: {
+ onBeforeAddons(manager, param) {
+ switch (param) {
+ case "disallowed":
+ setAndLockPref("dom.security.https_only_mode", false);
+ break;
+ case "enabled":
+ PoliciesUtils.setDefaultPref("dom.security.https_only_mode", true);
+ break;
+ case "force_enabled":
+ setAndLockPref("dom.security.https_only_mode", true);
+ break;
+ case "allowed":
+ // The default case.
+ break;
+ }
+ },
+ },
+
InstallAddonsPermission: {
onBeforeUIStartup(manager, param) {
if ("Allow" in param) {
@@ -1778,6 +1837,12 @@ export var Policies = {
},
},
+ PostQuantumKeyAgreementEnabled: {
+ onBeforeAddons(manager, param) {
+ setAndLockPref("security.tls.enable_kyber", param);
+ },
+ },
+
Preferences: {
onBeforeAddons(manager, param) {
let allowedPrefixes = [
@@ -1802,6 +1867,9 @@ export var Policies = {
"places.",
"pref.",
"print.",
+ "privacy.globalprivacycontrol.enabled",
+ "privacy.userContext.enabled",
+ "privacy.userContext.ui.enabled",
"signon.",
"spellchecker.",
"toolkit.legacyUserProfileCustomizations.stylesheets",
@@ -1820,6 +1888,8 @@ export var Policies = {
"security.insecure_connection_text.enabled",
"security.insecure_connection_text.pbmode.enabled",
"security.mixed_content.block_active_content",
+ "security.mixed_content.block_display_content",
+ "security.mixed_content.upgrade_display_content",
"security.osclientcerts.assume_rsa_pss_support",
"security.osclientcerts.autoload",
"security.OCSP.enabled",
@@ -1981,13 +2051,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 +2091,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 +2117,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 +2140,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 +2193,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 +2233,11 @@ export var Policies = {
param.SiteSettings,
locked
);
+ PoliciesUtils.setDefaultPref(
+ "privacy.clearOnShutdown_v2.siteSettings",
+ param.SiteSettings,
+ locked
+ );
}
if ("OfflineApps" in param) {
PoliciesUtils.setDefaultPref(
@@ -2390,15 +2508,14 @@ export var Policies = {
},
},
+ TranslateEnabled: {
+ onBeforeAddons(manager, param) {
+ setAndLockPref("browser.translations.enable", param);
+ },
+ },
+
UserMessaging: {
onBeforeAddons(manager, param) {
- if ("WhatsNew" in param) {
- PoliciesUtils.setDefaultPref(
- "browser.messaging-system.whatsNewPanel.enabled",
- param.WhatsNew,
- param.Locked
- );
- }
if ("ExtensionRecommendations" in param) {
PoliciesUtils.setDefaultPref(
"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
@@ -2790,7 +2907,7 @@ function clearBlockedAboutPages() {
gBlockedAboutPages = [];
}
-function blockAboutPage(manager, feature, neededOnContentProcess = false) {
+function blockAboutPage(manager, feature) {
addChromeURLBlocker();
gBlockedAboutPages.push(feature);
@@ -2826,7 +2943,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/content/aboutPolicies.html b/browser/components/enterprisepolicies/content/aboutPolicies.html
index bf0a962aa8..0b7a4e78ff 100644
--- a/browser/components/enterprisepolicies/content/aboutPolicies.html
+++ b/browser/components/enterprisepolicies/content/aboutPolicies.html
@@ -24,7 +24,6 @@
rel="localization"
href="browser/policies/policies-descriptions.ftl"
/>
- <link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<script src="chrome://browser/content/policies/aboutPolicies.js"></script>
</head>
diff --git a/browser/components/enterprisepolicies/content/aboutPolicies.js b/browser/components/enterprisepolicies/content/aboutPolicies.js
index 9cde085f3d..2de9be0982 100644
--- a/browser/components/enterprisepolicies/content/aboutPolicies.js
+++ b/browser/components/enterprisepolicies/content/aboutPolicies.js
@@ -294,6 +294,7 @@ function generateDocumentation() {
SanitizeOnShutdown: "SanitizeOnShutdown2",
WindowsSSO: "Windows10SSO",
SecurityDevices: "SecurityDevices2",
+ DisableFirefoxAccounts: "DisableFirefoxAccounts1",
};
for (let policyName in schema.properties) {
diff --git a/browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs b/browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs
index 5fc70c31cf..2e52a248c8 100644
--- a/browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs
+++ b/browser/components/enterprisepolicies/helpers/BookmarksPolicies.sys.mjs
@@ -204,44 +204,30 @@ async function insertBookmark(bookmark) {
}
function setFaviconForBookmark(bookmark) {
- let faviconURI;
- let nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({});
-
switch (bookmark.Favicon.protocol) {
- case "data:":
- // data urls must first call replaceFaviconDataFromDataURL, using a
- // fake URL. Later, it's needed to call setAndFetchFaviconForPage
- // with the same URL.
- faviconURI = Services.io.newURI("fake-favicon-uri:" + bookmark.URL.href);
-
- lazy.PlacesUtils.favicons.replaceFaviconDataFromDataURL(
- faviconURI,
- bookmark.Favicon.href,
- 0 /* max expiration length */,
- nullPrincipal
+ case "data:": {
+ lazy.PlacesUtils.favicons.setFaviconForPage(
+ bookmark.URL.URI,
+ Services.io.newURI("fake-favicon-uri:" + bookmark.URL.href),
+ bookmark.Favicon.URI
);
- break;
-
+ return;
+ }
case "http:":
- case "https:":
- faviconURI = Services.io.newURI(bookmark.Favicon.href);
- break;
-
- default:
- lazy.log.error(
- `Bad URL given for favicon on bookmark "${bookmark.Title}"`
+ case "https:": {
+ lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
+ bookmark.URL.URI,
+ bookmark.Favicon.URI,
+ false /* forceReload */,
+ lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ Services.scriptSecurityManager.createNullPrincipal({})
);
return;
+ }
}
- lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
- Services.io.newURI(bookmark.URL.href),
- faviconURI,
- false /* forceReload */,
- lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
- null,
- nullPrincipal
- );
+ lazy.log.error(`Bad URL given for favicon on bookmark "${bookmark.Title}"`);
}
// Cache of folder names to guids to be used by the getParentGuid
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..7058efb698 100644
--- a/browser/components/enterprisepolicies/schemas/policies-schema.json
+++ b/browser/components/enterprisepolicies/schemas/policies-schema.json
@@ -253,13 +253,22 @@
"DenyUrlRegexList": {
"type": "string"
},
+ "AgentName": {
+ "type": "string"
+ },
+ "ClientSignature": {
+ "type": "string"
+ },
"IsPerUser": {
"type": "boolean"
},
"ShowBlockedResult": {
"type": "boolean"
},
- "DefaultAllow": {
+ "DefaultResult": {
+ "type": "number"
+ },
+ "BypassForSameTabOperations": {
"type": "boolean"
}
}
@@ -420,6 +429,10 @@
"type": "boolean"
},
+ "DisableEncryptedClientHello": {
+ "type": "boolean"
+ },
+
"DisableFeedbackCommands": {
"type": "boolean"
},
@@ -662,6 +675,9 @@
"items": {
"type": "string"
}
+ },
+ "temporarily_allow_weak_signatures": {
+ "type": "boolean"
}
}
}
@@ -691,6 +707,9 @@
"default_area": {
"type": "string",
"enum": ["navbar", "menupanel"]
+ },
+ "temporarily_allow_weak_signatures": {
+ "type": "boolean"
}
}
}
@@ -820,6 +839,19 @@
}
},
+ "HttpAllowlist": {
+ "type": "array",
+ "strict": false,
+ "items": {
+ "type": "origin"
+ }
+ },
+
+ "HttpsOnlyMode": {
+ "type": "string",
+ "enum": ["allowed", "disallowed", "enabled", "force_enabled"]
+ },
+
"InstallAddonsPermission": {
"type": "object",
"properties": {
@@ -1168,6 +1200,10 @@
}
},
+ "PostQuantumKeyAgreementEnabled": {
+ "type": "boolean"
+ },
+
"Preferences": {
"type": ["object", "JSON"],
"patternProperties": {
@@ -1422,6 +1458,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_extensions.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensions.js
index 7d1313548b..6b8feb4d82 100644
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_extensions.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensions.js
@@ -40,7 +40,7 @@ add_task(async function test_addon_install() {
add_task(async function test_addon_locked() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
- const win = await BrowserOpenAddonsMgr("addons://list/extension");
+ const win = await BrowserAddonUI.openAddonsMgr("addons://list/extension");
await isExtensionLocked(win, ADDON_ID);
diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js
index 612448ee4e..dd610ec7e5 100644
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js
@@ -45,7 +45,7 @@ add_task(async function test_addon_install() {
add_task(async function test_addon_locked_update_disabled() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
- const win = await BrowserOpenAddonsMgr(
+ const win = await BrowserAddonUI.openAddonsMgr(
"addons://detail/" + encodeURIComponent(ADDON_ID)
);
diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_masterpassword.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_masterpassword.js
index 872eb5a652..87fb072971 100644
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_masterpassword.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_masterpassword.js
@@ -7,6 +7,25 @@ let { LoginTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/LoginTestUtils.sys.mjs"
);
+let { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+let { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+add_setup(async function () {
+ // Stub these out so we don't end up invoking the MP dialog
+ // in order to decrypt prefs to find out if these are enabled or disabled.
+ sinon.stub(FormAutofillUtils, "getOSAuthEnabled").returns(false);
+ sinon.stub(LoginHelper, "getOSAuthEnabled").returns(false);
+
+ registerCleanupFunction(async function () {
+ sinon.restore();
+ });
+});
+
// Test that once a password is set, you can't unset it
add_task(async function test_policy_masterpassword_set() {
await setupPolicyEngineWithJson({
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..76157c7e97 100644
--- a/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_extensionsettings.js
@@ -8,10 +8,14 @@ const { AddonTestUtils } = ChromeUtils.importESModule(
const { AddonManager } = ChromeUtils.importESModule(
"resource://gre/modules/AddonManager.sys.mjs"
);
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
AddonTestUtils.appInfo = getAppInfo();
+ExtensionTestUtils.init(this);
const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
const BASE_URL = `http://example.com/data`;
@@ -21,7 +25,35 @@ let themeID = "policytheme@mozilla.com";
let fileURL;
-add_task(async function setup() {
+async function assertManagementAPIInstallType(addonId, expectedInstallType) {
+ const addon = await AddonManager.getAddonByID(addonId);
+ const expectInstalledByPolicy = expectedInstallType === "admin";
+ equal(
+ addon.isInstalledByEnterprisePolicy,
+ expectInstalledByPolicy,
+ `Addon should ${
+ expectInstalledByPolicy ? "be" : "NOT be"
+ } marked as installed by enterprise policy`
+ );
+ const policy = WebExtensionPolicy.getByID(addonId);
+ const pageURL = policy.extension.baseURI.resolve(
+ "_generated_background_page.html"
+ );
+ const page = await ExtensionTestUtils.loadContentPage(pageURL);
+ const { id, installType } = await page.spawn([], async () => {
+ const res = await this.content.wrappedJSObject.browser.management.getSelf();
+ return { id: res.id, installType: res.installType };
+ });
+ await page.close();
+ Assert.equal(id, addonId, "Got results for the expected addon id");
+ Assert.equal(
+ installType,
+ expectedInstallType,
+ "Got the expected installType on policy installed extension"
+ );
+}
+
+add_setup(async function setup() {
await AddonTestUtils.promiseStartupManager();
let webExtensionFile = AddonTestUtils.createTempWebExtensionFile({
@@ -34,6 +66,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)
@@ -111,7 +147,14 @@ add_task(async function test_addon_allowed() {
);
await install.install();
notEqual(install.addon, null, "Addon should not be null");
+ await assertManagementAPIInstallType(install.addon.id, "normal");
equal(install.addon.appDisabled, false, "Addon should not be disabled");
+ equal(
+ install.addon.isInstalledByEnterprisePolicy,
+ false,
+ "Addon should NOT be marked as installed by enterprise policy"
+ );
+
await install.addon.uninstall();
});
@@ -165,6 +208,8 @@ add_task(async function test_addon_forceinstalled() {
0,
"Addon should not be able to be disabled."
);
+ await assertManagementAPIInstallType(addon.id, "admin");
+
await addon.uninstall();
});
@@ -195,6 +240,8 @@ add_task(async function test_addon_normalinstalled() {
0,
"Addon should be able to be disabled."
);
+ await assertManagementAPIInstallType(addon.id, "admin");
+
await addon.uninstall();
});
@@ -286,6 +333,116 @@ add_task(async function test_addon_normalinstalled_file() {
0,
"Addon should be able to be disabled."
);
+ await assertManagementAPIInstallType(addon.id, "admin");
+
+ 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_permissions.js b/browser/components/enterprisepolicies/tests/xpcshell/test_permissions.js
index f4440e53f5..c9231132c8 100644
--- a/browser/components/enterprisepolicies/tests/xpcshell/test_permissions.js
+++ b/browser/components/enterprisepolicies/tests/xpcshell/test_permissions.js
@@ -315,7 +315,7 @@ add_task(async function test_cookie_allow_session() {
);
});
-// This again seems out of places, but AutoLaunchProtocolsFromOrigins
+// This again seems out of place, but AutoLaunchProtocolsFromOrigins
// is all permissions.
add_task(async function test_autolaunchprotocolsfromorigins() {
await setupPolicyEngineWithJson({
@@ -337,7 +337,7 @@ add_task(async function test_autolaunchprotocolsfromorigins() {
);
});
-// This again seems out of places, but PasswordManagerExceptions
+// This again seems out of place, but PasswordManagerExceptions
// is all permissions.
add_task(async function test_passwordmanagerexceptions() {
await setupPolicyEngineWithJson({
@@ -353,3 +353,20 @@ add_task(async function test_passwordmanagerexceptions() {
Ci.nsIPermissionManager.DENY_ACTION
);
});
+
+// This again seems out of place, but HttpAllowlist
+// is all permissions.
+add_task(async function test_httpsonly_exceptions() {
+ await setupPolicyEngineWithJson({
+ policies: {
+ HttpAllowlist: ["https://http.example.com"],
+ },
+ });
+ equal(
+ PermissionTestUtils.testPermission(
+ URI("https://http.example.com"),
+ "https-only-load-insecure"
+ ),
+ Ci.nsIPermissionManager.ALLOW_ACTION
+ );
+});
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..cfcb655777 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,
},
},
@@ -615,13 +657,11 @@ const POLICIES_TESTS = [
{
policies: {
UserMessaging: {
- WhatsNew: false,
SkipOnboarding: true,
Locked: true,
},
},
lockedPrefs: {
- "browser.messaging-system.whatsNewPanel.enabled": false,
"browser.aboutwelcome.enabled": false,
},
},
@@ -1045,6 +1085,67 @@ 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,
+ },
+ },
+
+ // POLICY: DisableEncryptedClientHello
+ {
+ policies: {
+ DisableEncryptedClientHello: true,
+ },
+ lockedPrefs: {
+ "network.dns.echconfig.enabled": false,
+ "network.dns.http3_echconfig.enabled": false,
+ },
+ },
+
+ // POLICY: PostQuantumKeyAgreementEnabled
+ {
+ policies: {
+ PostQuantumKeyAgreementEnabled: false,
+ },
+ lockedPrefs: {
+ "security.tls.enable_kyber": false,
+ },
+ },
+
+ // POLICY: HttpsOnlyMode
+ {
+ policies: {
+ HttpsOnlyMode: "enabled",
+ },
+ unlockedPrefs: {
+ "dom.security.https_only_mode": true,
+ },
+ },
+
+ {
+ policies: {
+ HttpsOnlyMode: "disallowed",
+ },
+ lockedPrefs: {
+ "dom.security.https_only_mode": false,
+ },
+ },
+
+ {
+ policies: {
+ HttpsOnlyMode: "force_enabled",
+ },
+ lockedPrefs: {
+ "dom.security.https_only_mode": true,
+ },
+ },
];
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/ExtensionControlledPopup.sys.mjs b/browser/components/extensions/ExtensionControlledPopup.sys.mjs
index b07a8214f3..2d9a9fb584 100644
--- a/browser/components/extensions/ExtensionControlledPopup.sys.mjs
+++ b/browser/components/extensions/ExtensionControlledPopup.sys.mjs
@@ -235,8 +235,6 @@ export class ExtensionControlledPopup {
return;
}
- win.ownerGlobal.ensureCustomElements("moz-support-link");
-
// Find the elements we need.
let doc = win.document;
let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(doc);
diff --git a/browser/components/extensions/extension.css b/browser/components/extensions/extension.css
index f05e2f12a1..431f0148ae 100644
--- a/browser/components/extensions/extension.css
+++ b/browser/components/extensions/extension.css
@@ -21,7 +21,7 @@
body {
background: transparent;
box-sizing: border-box;
- color: #222426;
+ color: light-dark(#222426, CanvasText);
cursor: default;
display: flex;
flex-direction: column;
diff --git a/browser/components/extensions/parent/ext-browser.js b/browser/components/extensions/parent/ext-browser.js
index 7b01d15101..e7a516dcd3 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();
@@ -78,7 +82,7 @@ global.openOptionsPage = extension => {
extension.id
)}/preferences`;
- return window.BrowserOpenAddonsMgr(viewId);
+ return window.BrowserAddonUI.openAddonsMgr(viewId);
};
global.makeWidgetId = id => {
@@ -705,6 +709,23 @@ class TabTracker extends TabTrackerBase {
};
}
+ getBrowserDataForContext(context) {
+ if (["tab", "background"].includes(context.viewType)) {
+ return this.getBrowserData(context.xulBrowser);
+ } else if (["popup", "sidebar"].includes(context.viewType)) {
+ // popups and sidebars are nested inside a browser element
+ // (with url "chrome://browser/content/webext-panels.xhtml")
+ // and so we look for the corresponding topChromeWindow to
+ // determine the windowId the panel belongs to.
+ const chromeWindow =
+ context.xulBrowser?.ownerGlobal?.browsingContext?.topChromeWindow;
+ const windowId = chromeWindow ? windowTracker.getId(chromeWindow) : -1;
+ return { tabId: -1, windowId };
+ }
+
+ return { tabId: -1, windowId: -1 };
+ }
+
get activeTab() {
let window = windowTracker.topWindow;
if (window && window.gBrowser) {
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-menus.js b/browser/components/extensions/parent/ext-menus.js
index a5b27bff7d..e4ad9f4747 100644
--- a/browser/components/extensions/parent/ext-menus.js
+++ b/browser/components/extensions/parent/ext-menus.js
@@ -1100,11 +1100,11 @@ const menuTracker = {
);
sidebarHeader.addEventListener("SidebarShown", menuTracker.onSidebarShown);
- await window.SidebarUI.promiseInitialized;
+ await window.SidebarController.promiseInitialized;
if (
!window.closed &&
- window.SidebarUI.currentID === "viewBookmarksSidebar"
+ window.SidebarController.currentID === "viewBookmarksSidebar"
) {
menuTracker.onSidebarShown({ currentTarget: sidebarHeader });
}
@@ -1121,8 +1121,8 @@ const menuTracker = {
);
sidebarHeader.removeEventListener("SidebarShown", this.onSidebarShown);
- if (window.SidebarUI.currentID === "viewBookmarksSidebar") {
- let sidebarBrowser = window.SidebarUI.browser;
+ if (window.SidebarController.currentID === "viewBookmarksSidebar") {
+ let sidebarBrowser = window.SidebarController.browser;
sidebarBrowser.removeEventListener("load", this.onSidebarShown);
const menu =
sidebarBrowser.contentDocument.getElementById("placesContext");
@@ -1134,10 +1134,10 @@ const menuTracker = {
// The event target is an element in a browser window, so |window| will be
// the browser window that contains the sidebar.
const window = event.currentTarget.ownerGlobal;
- if (window.SidebarUI.currentID === "viewBookmarksSidebar") {
- let sidebarBrowser = window.SidebarUI.browser;
+ if (window.SidebarController.currentID === "viewBookmarksSidebar") {
+ let sidebarBrowser = window.SidebarController.browser;
if (sidebarBrowser.contentDocument.readyState !== "complete") {
- // SidebarUI.currentID may be updated before the bookmark sidebar's
+ // SidebarController.currentID may be updated before the bookmark sidebar's
// document has finished loading. This sometimes happens when the
// sidebar is automatically shown when a new window is opened.
sidebarBrowser.addEventListener("load", menuTracker.onSidebarShown, {
diff --git a/browser/components/extensions/parent/ext-sidebarAction.js b/browser/components/extensions/parent/ext-sidebarAction.js
index 197456abd9..b2c009014e 100644
--- a/browser/components/extensions/parent/ext-sidebarAction.js
+++ b/browser/components/extensions/parent/ext-sidebarAction.js
@@ -17,8 +17,6 @@ var { IconDetails } = ExtensionParent;
// WeakMap[Extension -> SidebarAction]
let sidebarActionMap = new WeakMap();
-const sidebarURL = "chrome://browser/content/webext-panels.xhtml";
-
/**
* Responsible for the sidebar_action section of the manifest as well
* as the associated sidebar browser.
@@ -40,7 +38,6 @@ this.sidebarAction = class extends ExtensionAPI {
let widgetId = makeWidgetId(extension.id);
this.id = `${widgetId}-sidebar-action`;
this.menuId = `menubar_menu_${this.id}`;
- this.switcherMenuId = `sidebarswitcher_menu_${this.id}`;
this.browserStyle = options.browser_style;
@@ -66,23 +63,6 @@ this.sidebarAction = class extends ExtensionAPI {
};
windowTracker.addOpenListener(this.windowOpenListener);
- this.updateHeader = event => {
- let window = event.target.ownerGlobal;
- let details = this.tabContext.get(window.gBrowser.selectedTab);
- let header = window.document.getElementById("sidebar-switcher-target");
- if (window.SidebarUI.currentID === this.id) {
- this.setMenuIcon(header, details);
- }
- };
-
- this.windowCloseListener = window => {
- let header = window.document.getElementById("sidebar-switcher-target");
- if (header) {
- header.removeEventListener("SidebarShown", this.updateHeader);
- }
- };
- windowTracker.addCloseListener(this.windowCloseListener);
-
sidebarActionMap.set(extension, this);
}
@@ -91,7 +71,7 @@ this.sidebarAction = class extends ExtensionAPI {
}
onShutdown(isAppShutdown) {
- sidebarActionMap.delete(this.this);
+ sidebarActionMap.delete(this.extension);
this.tabContext.shutdown();
@@ -102,26 +82,18 @@ this.sidebarAction = class extends ExtensionAPI {
}
for (let window of windowTracker.browserWindows()) {
- let { document, SidebarUI } = window;
- if (SidebarUI.currentID === this.id) {
- SidebarUI.hide();
- }
- document.getElementById(this.menuId)?.remove();
- document.getElementById(this.switcherMenuId)?.remove();
- let header = document.getElementById("sidebar-switcher-target");
- header.removeEventListener("SidebarShown", this.updateHeader);
- SidebarUI.sidebars.delete(this.id);
+ let { SidebarController } = window;
+ SidebarController.removeExtension(this.id);
}
windowTracker.removeOpenListener(this.windowOpenListener);
- windowTracker.removeCloseListener(this.windowCloseListener);
}
static onUninstall(id) {
const sidebarId = `${makeWidgetId(id)}-sidebar-action`;
for (let window of windowTracker.browserWindows()) {
- let { SidebarUI } = window;
- if (SidebarUI.lastOpenedId === sidebarId) {
- SidebarUI.lastOpenedId = null;
+ let { SidebarController } = window;
+ if (SidebarController.lastOpenedId === sidebarId) {
+ SidebarController.lastOpenedId = null;
}
}
}
@@ -135,12 +107,12 @@ this.sidebarAction = class extends ExtensionAPI {
let install = this.extension.startupReason === "ADDON_INSTALL";
for (let window of windowTracker.browserWindows()) {
this.updateWindow(window);
- let { SidebarUI } = window;
+ let { SidebarController } = window;
if (
(install && this.extension.manifest.sidebar_action.open_at_install) ||
- SidebarUI.lastOpenedId == this.id
+ SidebarController.lastOpenedId == this.id
) {
- SidebarUI.show(this.id);
+ SidebarController.show(this.id);
}
}
}
@@ -149,60 +121,29 @@ this.sidebarAction = class extends ExtensionAPI {
if (!this.extension.canAccessWindow(window)) {
return;
}
- let { document, SidebarUI } = window;
- let keyId = `ext-key-id-${this.id}`;
-
- SidebarUI.sidebars.set(this.id, {
- title: details.title,
- url: sidebarURL,
+ this.panel = details.panel;
+ let { SidebarController } = window;
+ SidebarController.registerExtension(this.id, {
+ icon: this.getMenuIcon(details),
menuId: this.menuId,
- switcherMenuId: this.switcherMenuId,
- // The following properties are specific to extensions
+ title: details.title,
extensionId: this.extension.id,
- panel: details.panel,
- browserStyle: this.browserStyle,
+ onload: () =>
+ SidebarController.browser.contentWindow.loadPanel(
+ this.extension.id,
+ this.panel,
+ this.browserStyle
+ ),
});
-
- let header = document.getElementById("sidebar-switcher-target");
- header.addEventListener("SidebarShown", this.updateHeader);
-
- // Insert a menuitem for View->Show Sidebars.
- let menuitem = document.createXULElement("menuitem");
- menuitem.setAttribute("id", this.menuId);
- menuitem.setAttribute("type", "checkbox");
- menuitem.setAttribute("label", details.title);
- menuitem.setAttribute("oncommand", `SidebarUI.toggle("${this.id}");`);
- menuitem.setAttribute("class", "menuitem-iconic webextension-menuitem");
- menuitem.setAttribute("key", keyId);
- this.setMenuIcon(menuitem, details);
-
- // Insert a toolbarbutton for the sidebar dropdown selector.
- let switcherMenuitem = menuitem.cloneNode();
- switcherMenuitem.setAttribute("id", this.switcherMenuId);
- switcherMenuitem.removeAttribute("type");
-
- document.getElementById("viewSidebarMenu").appendChild(menuitem);
- let separator = document.getElementById("sidebar-extensions-separator");
- separator.parentNode.insertBefore(switcherMenuitem, separator);
-
- return menuitem;
}
- setMenuIcon(menuitem, details) {
+ getMenuIcon(details) {
let getIcon = size =>
IconDetails.escapeUrl(
IconDetails.getPreferredIcon(details.icon, this.extension, size).icon
);
- menuitem.setAttribute(
- "style",
- `
- --webextension-menuitem-image: image-set(
- url("${getIcon(16)}"),
- url("${getIcon(32)}") 2x
- );
- `
- );
+ return `image-set(url("${getIcon(16)}"), url("${getIcon(32)}") 2x)`;
}
/**
@@ -214,34 +155,26 @@ this.sidebarAction = class extends ExtensionAPI {
* Tab specific sidebar configuration.
*/
updateButton(window, tabData) {
- let { document, SidebarUI } = window;
+ let { document, SidebarController } = window;
let title = tabData.title || this.extension.name;
- let menu = document.getElementById(this.menuId);
- if (!menu) {
- menu = this.createMenuItem(window, tabData);
+ if (!document.getElementById(this.menuId)) {
+ // Menu items are added when new windows are opened, or from onReady (when
+ // an extension has fully started). The menu item may be missing at this
+ // point if the extension updates the sidebar during its startup.
+ this.createMenuItem(window, tabData);
}
-
- let urlChanged = tabData.panel !== SidebarUI.sidebars.get(this.id).panel;
+ let urlChanged = tabData.panel !== this.panel;
if (urlChanged) {
- SidebarUI.sidebars.get(this.id).panel = tabData.panel;
- }
-
- menu.setAttribute("label", title);
- this.setMenuIcon(menu, tabData);
-
- let button = document.getElementById(this.switcherMenuId);
- button.setAttribute("label", title);
- this.setMenuIcon(button, tabData);
-
- // Update the sidebar if this extension is the current sidebar.
- if (SidebarUI.currentID === this.id) {
- SidebarUI.title = title;
- let header = document.getElementById("sidebar-switcher-target");
- this.setMenuIcon(header, tabData);
- if (SidebarUI.isOpen && urlChanged) {
- SidebarUI.show(this.id);
- }
+ this.panel = tabData.panel;
}
+ SidebarController.setExtensionAttributes(
+ this.id,
+ {
+ icon: this.getMenuIcon(tabData),
+ label: title,
+ },
+ urlChanged
+ );
}
/**
@@ -382,9 +315,9 @@ this.sidebarAction = class extends ExtensionAPI {
* @param {ChromeWindow} window
*/
triggerAction(window) {
- let { SidebarUI } = window;
- if (SidebarUI && this.extension.canAccessWindow(window)) {
- SidebarUI.toggle(this.id);
+ let { SidebarController } = window;
+ if (SidebarController && this.extension.canAccessWindow(window)) {
+ SidebarController.toggle(this.id);
}
}
@@ -394,9 +327,9 @@ this.sidebarAction = class extends ExtensionAPI {
* @param {ChromeWindow} window
*/
open(window) {
- let { SidebarUI } = window;
- if (SidebarUI && this.extension.canAccessWindow(window)) {
- SidebarUI.show(this.id);
+ let { SidebarController } = window;
+ if (SidebarController && this.extension.canAccessWindow(window)) {
+ SidebarController.show(this.id);
}
}
@@ -407,7 +340,7 @@ this.sidebarAction = class extends ExtensionAPI {
*/
close(window) {
if (this.isOpen(window)) {
- window.SidebarUI.hide();
+ window.SidebarController.hide();
}
}
@@ -417,15 +350,15 @@ this.sidebarAction = class extends ExtensionAPI {
* @param {ChromeWindow} window
*/
toggle(window) {
- let { SidebarUI } = window;
- if (!SidebarUI || !this.extension.canAccessWindow(window)) {
+ let { SidebarController } = window;
+ if (!SidebarController || !this.extension.canAccessWindow(window)) {
return;
}
if (!this.isOpen(window)) {
- SidebarUI.show(this.id);
+ SidebarController.show(this.id);
} else {
- SidebarUI.hide();
+ SidebarController.hide();
}
}
@@ -436,8 +369,8 @@ this.sidebarAction = class extends ExtensionAPI {
* @returns {boolean}
*/
isOpen(window) {
- let { SidebarUI } = window;
- return SidebarUI.isOpen && this.id == SidebarUI.currentID;
+ let { SidebarController } = window;
+ return SidebarController.isOpen && this.id == SidebarController.currentID;
}
getAPI(context) {
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)[&lt;all_urls&gt;] permission to use this method.",
- "permissions": ["<all_urls>"],
+ "description": "Captures an area of the currently active tab in the specified window. You must have &lt;all_urls&gt; 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..b5b4322ffe 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"]
@@ -389,6 +392,8 @@ run-if = ["crashreporter"]
["browser_ext_request_permissions.js"]
+["browser_ext_runtime_getContexts.js"]
+
["browser_ext_runtime_onPerformanceWarning.js"]
["browser_ext_runtime_openOptionsPage.js"]
@@ -700,7 +705,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..32de8b95f2 100644
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_contextMenu.js
@@ -2,10 +2,6 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
-ChromeUtils.defineESModuleGetters(this, {
- AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
-});
-
XPCOMUtils.defineLazyPreferenceGetter(
this,
"ABUSE_REPORT_ENABLED",
@@ -430,9 +426,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 +459,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;
}
@@ -552,35 +542,6 @@ async function browseraction_contextmenu_report_extension_helper() {
useAddonManager: "temporary",
});
- async function testReportDialog(viaUnifiedContextMenu) {
- const reportDialogWindow = await BrowserTestUtils.waitForCondition(
- () => AbuseReporter.getOpenDialog(),
- "Wait for the abuse report dialog to have been opened"
- );
-
- const reportDialogParams = reportDialogWindow.arguments[0].wrappedJSObject;
- is(
- reportDialogParams.report.addon.id,
- id,
- "Abuse report dialog has the expected addon id"
- );
- is(
- reportDialogParams.report.reportEntryPoint,
- viaUnifiedContextMenu ? "unified_context_menu" : "toolbar_context_menu",
- "Abuse report dialog has the expected reportEntryPoint"
- );
-
- info("Wait the report dialog to complete rendering");
- await reportDialogParams.promiseReportPanel;
- info("Close the report dialog");
- reportDialogWindow.close();
- is(
- await reportDialogParams.promiseReport,
- undefined,
- "Report resolved as user cancelled when the window is closed"
- );
- }
-
async function testContextMenu(menuId, customizing) {
info(`Open browserAction context menu in ${menuId}`);
let menu = await openContextMenu(menuId, buttonId);
@@ -597,54 +558,27 @@ async function browseraction_contextmenu_report_extension_helper() {
let aboutAddonsBrowser;
- if (AbuseReporter.amoFormEnabled) {
- const reportURL = Services.urlFormatter
- .formatURLPref("extensions.abuseReport.amoFormURL")
- .replace("%addonID%", id);
-
- const promiseReportTab = BrowserTestUtils.waitForNewTab(
- gBrowser,
- reportURL,
- /* waitForLoad */ false,
- // Expect it to be the next tab opened
- /* waitForAnyTab */ false
- );
- await closeChromeContextMenu(menuId, reportExtension);
- const reportTab = await promiseReportTab;
- // Remove the report tab and expect the selected tab
- // to become the about:addons tab.
- BrowserTestUtils.removeTab(reportTab);
- is(
- gBrowser.selectedBrowser.currentURI.spec,
- "about:blank",
- "Expect about:addons tab to not have been opened (amoFormEnabled=true)"
- );
- } else {
- // When running in customizing mode "about:addons" will load in a new tab,
- // otherwise it will replace the existing blank tab.
- const onceAboutAddonsTab = customizing
- ? BrowserTestUtils.waitForNewTab(gBrowser, "about:addons")
- : BrowserTestUtils.waitForCondition(() => {
- return gBrowser.currentURI.spec === "about:addons";
- }, "Wait an about:addons tab to be opened");
- await closeChromeContextMenu(menuId, reportExtension);
- await onceAboutAddonsTab;
- const browser = gBrowser.selectedBrowser;
- is(
- browser.currentURI.spec,
- "about:addons",
- "Got about:addons tab selected (amoFormEnabled=false)"
- );
- // Do not wait for the about:addons tab to be loaded if its
- // document is already readyState==complete.
- // This prevents intermittent timeout failures while running
- // this test in optimized builds.
- if (browser.contentDocument?.readyState != "complete") {
- await BrowserTestUtils.browserLoaded(browser);
- }
- await testReportDialog(usingUnifiedContextMenu);
- aboutAddonsBrowser = browser;
- }
+ const reportURL = Services.urlFormatter
+ .formatURLPref("extensions.abuseReport.amoFormURL")
+ .replace("%addonID%", id);
+
+ const promiseReportTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ reportURL,
+ /* waitForLoad */ false,
+ // Expect it to be the next tab opened
+ /* waitForAnyTab */ false
+ );
+ await closeChromeContextMenu(menuId, reportExtension);
+ const reportTab = await promiseReportTab;
+ // Remove the report tab and expect the selected tab
+ // to become the about:addons tab.
+ BrowserTestUtils.removeTab(reportTab);
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:blank",
+ "Expect about:addons tab to not have been opened"
+ );
// Close the new about:addons tab when running in customize mode,
// or cancel the abuse report if the about:addons page has been
@@ -735,22 +669,7 @@ add_task(async function test_unified_extensions_ui() {
await browseraction_contextmenu_manage_extension_helper();
await browseraction_contextmenu_remove_extension_helper();
await test_no_toolbar_pinning_on_builtin_helper();
-});
-
-add_task(async function test_report_amoFormEnabled() {
- await SpecialPowers.pushPrefEnv({
- set: [["extensions.abuseReport.amoFormEnabled", true]],
- });
- await browseraction_contextmenu_report_extension_helper();
- await SpecialPowers.popPrefEnv();
-});
-
-add_task(async function test_report_amoFormDisabled() {
- await SpecialPowers.pushPrefEnv({
- set: [["extensions.abuseReport.amoFormEnabled", false]],
- });
await browseraction_contextmenu_report_extension_helper();
- await SpecialPowers.popPrefEnv();
});
/**
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_execute_browser_action.js b/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
index 526dfbbeeb..9ca2a9c666 100644
--- a/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
+++ b/browser/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js
@@ -2,10 +2,9 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
-add_task(async function testTabSwitchActionContext() {
- await SpecialPowers.pushPrefEnv({
- set: [["extensions.manifestV3.enabled", true]],
- });
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
});
async function testExecuteBrowserActionWithOptions(options = {}) {
@@ -192,3 +191,232 @@ add_task(
});
}
);
+
+add_task(async function test_fallback_to_execute_browser_action_in_mv3() {
+ // Make sure the mouse isn't hovering over the browserAction widget.
+ EventUtils.synthesizeMouseAtCenter(
+ gURLBar.textbox,
+ { type: "mouseover" },
+ window
+ );
+
+ const EXTENSION_ID = "@test-action";
+ const extMV2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ browser_specific_settings: { gecko: { id: EXTENSION_ID } },
+ commands: {
+ _execute_browser_action: {
+ suggested_key: {
+ default: "Alt+1",
+ },
+ },
+ },
+ },
+ async background() {
+ await browser.commands.update({
+ name: "_execute_browser_action",
+ shortcut: "Alt+Shift+2",
+ });
+ browser.test.sendMessage("command-update");
+ },
+ useAddonManager: "temporary",
+ });
+ await extMV2.startup();
+ await extMV2.awaitMessage("command-update");
+
+ let storedCommands = ExtensionSettingsStore.getAllForExtension(
+ EXTENSION_ID,
+ "commands"
+ );
+ Assert.deepEqual(
+ storedCommands,
+ ["_execute_browser_action"],
+ "expected a stored command"
+ );
+
+ const extDataMV3 = {
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ browser_specific_settings: { gecko: { id: EXTENSION_ID } },
+ commands: {
+ _execute_action: {
+ suggested_key: {
+ default: "Alt+3",
+ },
+ },
+ },
+ },
+ background() {
+ browser.action.onClicked.addListener(() => {
+ browser.test.notifyPass("execute-action-on-clicked-fired");
+ });
+
+ browser.test.onMessage.addListener(async (msg, data) => {
+ switch (msg) {
+ case "verify": {
+ const commands = await browser.commands.getAll();
+ browser.test.assertDeepEq(
+ data,
+ commands,
+ "expected correct commands"
+ );
+ browser.test.sendMessage(`${msg}-done`);
+ break;
+ }
+
+ case "update":
+ await browser.commands.update(data);
+ browser.test.sendMessage(`${msg}-done`);
+ break;
+
+ case "reset":
+ await browser.commands.reset("_execute_action");
+ browser.test.sendMessage(`${msg}-done`);
+ break;
+
+ default:
+ browser.test.fail(`unexpected message: ${msg}`);
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ },
+ useAddonManager: "temporary",
+ };
+ const extMV3 = ExtensionTestUtils.loadExtension(extDataMV3);
+ await extMV3.startup();
+ await extMV3.awaitMessage("ready");
+
+ // We should have the shortcut value from the previous extension.
+ extMV3.sendMessage("verify", [
+ {
+ name: "_execute_action",
+ description: null,
+ shortcut: "Alt+Shift+2",
+ },
+ ]);
+ await extMV3.awaitMessage("verify-done");
+
+ // Execute the shortcut from the MV2 extension.
+ await SimpleTest.promiseFocus(window);
+ EventUtils.synthesizeKey("2", {
+ altKey: true,
+ shiftKey: true,
+ });
+ await extMV3.awaitFinish("execute-action-on-clicked-fired");
+
+ // Update the shortcut.
+ extMV3.sendMessage("update", {
+ name: "_execute_action",
+ shortcut: "Alt+Shift+4",
+ });
+ await extMV3.awaitMessage("update-done");
+
+ extMV3.sendMessage("verify", [
+ {
+ name: "_execute_action",
+ description: null,
+ shortcut: "Alt+Shift+4",
+ },
+ ]);
+ await extMV3.awaitMessage("verify-done");
+
+ // At this point, we should have the old and new commands in storage.
+ storedCommands = ExtensionSettingsStore.getAllForExtension(
+ EXTENSION_ID,
+ "commands"
+ );
+ Assert.deepEqual(
+ storedCommands,
+ ["_execute_browser_action", "_execute_action"],
+ "expected two stored commands"
+ );
+
+ // Disarm any pending writes before we modify the JSONFile directly.
+ await ExtensionSettingsStore._reloadFile(
+ true // saveChanges
+ );
+
+ let jsonFileName = "extension-settings.json";
+ let storePath = PathUtils.join(PathUtils.profileDir, jsonFileName);
+
+ let settingsStoreData = await IOUtils.readJSON(storePath);
+ Assert.deepEqual(
+ Array.from(Object.keys(settingsStoreData.commands)),
+ ["_execute_browser_action", "_execute_action"],
+ "expected command hortcuts data to be found in extension-settings.json"
+ );
+
+ // Reverse the order of _execute_action and _execute_browser_action stored
+ // in the settings store.
+ settingsStoreData.commands = {
+ _execute_action: settingsStoreData.commands._execute_action,
+ _execute_browser_action: settingsStoreData.commands._execute_browser_action,
+ };
+
+ Assert.deepEqual(
+ Array.from(Object.keys(settingsStoreData.commands)),
+ ["_execute_action", "_execute_browser_action"],
+ "expected command shortcuts order to be reversed in extension-settings.json data"
+ );
+
+ // Write the extension-settings.json data and reload it.
+ await IOUtils.writeJSON(storePath, settingsStoreData);
+ await ExtensionSettingsStore._reloadFile(
+ false // saveChanges
+ );
+
+ // Restart the extension to verify that the loaded command is the right one.
+ const updatedExtMV3 = ExtensionTestUtils.loadExtension(extDataMV3);
+ await updatedExtMV3.startup();
+ await updatedExtMV3.awaitMessage("ready");
+
+ // We should *still* have two stored commands.
+ storedCommands = ExtensionSettingsStore.getAllForExtension(
+ EXTENSION_ID,
+ "commands"
+ );
+ Assert.deepEqual(
+ storedCommands,
+ ["_execute_action", "_execute_browser_action"],
+ "expected two stored commands"
+ );
+
+ updatedExtMV3.sendMessage("verify", [
+ {
+ name: "_execute_action",
+ description: null,
+ shortcut: "Alt+Shift+4",
+ },
+ ]);
+ await updatedExtMV3.awaitMessage("verify-done");
+
+ updatedExtMV3.sendMessage("reset");
+ await updatedExtMV3.awaitMessage("reset-done");
+
+ // Resetting the shortcut should take the default value from the latest
+ // extension version.
+ updatedExtMV3.sendMessage("verify", [
+ {
+ name: "_execute_action",
+ description: null,
+ shortcut: "Alt+3",
+ },
+ ]);
+ await updatedExtMV3.awaitMessage("verify-done");
+
+ // At this point, we should no longer have any stored commands, since we are
+ // using the default.
+ storedCommands = ExtensionSettingsStore.getAllForExtension(
+ EXTENSION_ID,
+ "commands"
+ );
+ Assert.deepEqual(storedCommands, [], "expected no stored command");
+
+ await extMV2.unload();
+ await extMV3.unload();
+ await updatedExtMV3.unload();
+});
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_commands_update.js b/browser/components/extensions/test/browser/browser_ext_commands_update.js
index 5e1f05e346..9598328d26 100644
--- a/browser/components/extensions/test/browser/browser_ext_commands_update.js
+++ b/browser/components/extensions/test/browser/browser_ext_commands_update.js
@@ -401,11 +401,11 @@ add_task(async function updateSidebarCommand() {
await extension.awaitMessage("sidebar");
// Show and hide the switcher panel to generate the initial shortcuts.
- let switcherShown = promisePopupShown(SidebarUI._switcherPanel);
- SidebarUI.showSwitcherPanel();
+ let switcherShown = promisePopupShown(SidebarController._switcherPanel);
+ SidebarController.showSwitcherPanel();
await switcherShown;
- let switcherHidden = promisePopupHidden(SidebarUI._switcherPanel);
- SidebarUI.hideSwitcherPanel();
+ let switcherHidden = promisePopupHidden(SidebarController._switcherPanel);
+ SidebarController.hideSwitcherPanel();
await switcherHidden;
let menuitemId = `sidebarswitcher_menu_${makeWidgetId(
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.js b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
index 77e4bf0827..0ed5d6ce9e 100644
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -724,7 +724,7 @@ add_task(async function test_bookmark_sidebar_contextmenu() {
await extension.startup();
let bookmarkGuid = await extension.awaitMessage("bookmark-created");
- let sidebar = window.SidebarUI.browser;
+ let sidebar = window.SidebarController.browser;
let menu = sidebar.contentDocument.getElementById("placesContext");
tree.selectItems([bookmarkGuid]);
let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
diff --git a/browser/components/extensions/test/browser/browser_ext_contextMenus_bookmarks.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_bookmarks.js
index 24411731f7..3c0a1269a9 100644
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus_bookmarks.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_bookmarks.js
@@ -54,7 +54,7 @@ add_task(async function test_bookmark_sidebar_contextmenu() {
expectedVirtualID,
] of expected_bookmarkID_2_virtualID) {
info(`Testing context menu for Bookmark ID "${expectedBookmarkID}"`);
- let sidebar = window.SidebarUI.browser;
+ let sidebar = window.SidebarController.browser;
let menu = sidebar.contentDocument.getElementById("placesContext");
tree.selectItems([expectedBookmarkID]);
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_contextMenus_targetUrlPatterns.js b/browser/components/extensions/test/browser/browser_ext_contextMenus_targetUrlPatterns.js
index 0f353600d7..ae1ec31827 100644
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus_targetUrlPatterns.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_targetUrlPatterns.js
@@ -221,7 +221,7 @@ add_task(async function privileged_are_allowed_to_use_restrictedSchemes() {
manifest: {
permissions: ["tabs", "contextMenus", "mozillaAddons"],
},
- async background() {
+ background() {
browser.contextMenus.create({
id: "privileged-extension",
title: "Privileged Extension",
@@ -229,6 +229,7 @@ add_task(async function privileged_are_allowed_to_use_restrictedSchemes() {
documentUrlPatterns: ["about:reader*"],
});
+ let articleReady = Promise.withResolvers();
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (
changeInfo.status === "complete" &&
@@ -236,6 +237,9 @@ add_task(async function privileged_are_allowed_to_use_restrictedSchemes() {
) {
browser.test.sendMessage("readerModeEntered");
}
+ if (tab.isArticle && tab.url.includes("/readerModeArticle.html")) {
+ articleReady.resolve();
+ }
});
browser.test.onMessage.addListener(async msg => {
@@ -244,8 +248,20 @@ add_task(async function privileged_are_allowed_to_use_restrictedSchemes() {
return;
}
+ browser.test.log("Waiting for tab.isArticle to be true");
+ await articleReady.promise;
+ browser.test.log("Toggling reader mode");
browser.tabs.toggleReaderMode();
});
+
+ browser.tabs.query(
+ { url: "*://example.com/*/readerModeArticle.html" },
+ tabs => {
+ if (tabs[0].isArticle) {
+ articleReady.resolve();
+ }
+ }
+ );
},
});
diff --git a/browser/components/extensions/test/browser/browser_ext_getViews.js b/browser/components/extensions/test/browser/browser_ext_getViews.js
index 1af190e753..20db9c86c8 100644
--- a/browser/components/extensions/test/browser/browser_ext_getViews.js
+++ b/browser/components/extensions/test/browser/browser_ext_getViews.js
@@ -78,44 +78,6 @@ function genericChecker() {
browser.test.sendMessage(kind + "-ready");
}
-async function promiseBrowserContentUnloaded(browser) {
- // Wait until the content has unloaded before resuming the test, to avoid
- // calling extension.getViews too early (and having intermittent failures).
- const MSG_WINDOW_DESTROYED = "Test:BrowserContentDestroyed";
- let unloadPromise = new Promise(resolve => {
- Services.ppmm.addMessageListener(MSG_WINDOW_DESTROYED, function listener() {
- Services.ppmm.removeMessageListener(MSG_WINDOW_DESTROYED, listener);
- resolve();
- });
- });
-
- await ContentTask.spawn(
- browser,
- MSG_WINDOW_DESTROYED,
- MSG_WINDOW_DESTROYED => {
- let innerWindowId = this.content.windowGlobalChild.innerWindowId;
- let observer = subject => {
- if (
- innerWindowId === subject.QueryInterface(Ci.nsISupportsPRUint64).data
- ) {
- Services.obs.removeObserver(observer, "inner-window-destroyed");
-
- // Use process message manager to ensure that the message is delivered
- // even after the <browser>'s message manager is disconnected.
- Services.cpmm.sendAsyncMessage(MSG_WINDOW_DESTROYED);
- }
- };
- // Observe inner-window-destroyed, like ExtensionPageChild, to ensure that
- // the ExtensionPageContextChild instance has been unloaded when we resolve
- // the unloadPromise.
- Services.obs.addObserver(observer, "inner-window-destroyed");
- }
- );
-
- // Return an object so that callers can use "await".
- return { unloadPromise };
-}
-
add_task(async function () {
let win1 = await BrowserTestUtils.openNewBrowserWindow();
let win2 = await BrowserTestUtils.openNewBrowserWindow();
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js
index c9627c5ae9..7960dd74dc 100644
--- a/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus_replace_menu_permissions.js
@@ -189,7 +189,9 @@ add_task(async function overrideContext_permissions() {
// permissions.request requires user input, export helper.
await SpecialPowers.spawn(
- SidebarUI.browser.contentDocument.getElementById("webext-panels-browser"),
+ SidebarController.browser.contentDocument.getElementById(
+ "webext-panels-browser"
+ ),
[],
() => {
const { ExtensionCommon } = ChromeUtils.importESModule(
@@ -212,7 +214,9 @@ add_task(async function overrideContext_permissions() {
await BrowserTestUtils.synthesizeMouseAtCenter(
"a",
{ type: "contextmenu" },
- SidebarUI.browser.contentDocument.getElementById("webext-panels-browser")
+ SidebarController.browser.contentDocument.getElementById(
+ "webext-panels-browser"
+ )
);
} while (await extension.awaitMessage("continue_test"));
diff --git a/browser/components/extensions/test/browser/browser_ext_menus_targetElement.js b/browser/components/extensions/test/browser/browser_ext_menus_targetElement.js
index 4fc9ebba82..9035b6eecb 100644
--- a/browser/components/extensions/test/browser/browser_ext_menus_targetElement.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus_targetElement.js
@@ -308,6 +308,14 @@ add_task(async function independentMenusInDifferentTabs() {
gBrowser.selectedTab = tab2;
let targetElementId2 = await extension.openAndCloseMenu("#editabletext");
+ if (targetElementId === targetElementId2) {
+ // targetElementId is only guaranteed to be unique within a tab, so odds are
+ // that by unlucky coincidence, that the discovered ID accidentally overlaps
+ // with the actual ID.
+ info(`Got same targetElementId ${targetElementId}, retrying for a new one`);
+ targetElementId2 = await extension.openAndCloseMenu("#editabletext");
+ }
+ Assert.notEqual(targetElementId, targetElementId2, "targetElementId differ");
await extension.checkIsValid(
targetElementId2,
diff --git a/browser/components/extensions/test/browser/browser_ext_mousewheel_zoom.js b/browser/components/extensions/test/browser/browser_ext_mousewheel_zoom.js
index 9c5f447a76..1c9224ca00 100644
--- a/browser/components/extensions/test/browser/browser_ext_mousewheel_zoom.js
+++ b/browser/components/extensions/test/browser/browser_ext_mousewheel_zoom.js
@@ -120,7 +120,7 @@ async function test_mousewheel_zoom(test) {
const sidebar = document.getElementById("sidebar-box");
ok(!sidebar.hidden, "Sidebar box is visible");
- browser = SidebarUI.browser.contentWindow.gBrowser.selectedBrowser;
+ browser = SidebarController.browser.contentWindow.gBrowser.selectedBrowser;
} else if (test == TESTS.BROWSER_ACTION) {
browser = await openBrowserActionPanel(extension, undefined, true);
} else if (test == TESTS.PAGE_ACTION) {
diff --git a/browser/components/extensions/test/browser/browser_ext_openPanel.js b/browser/components/extensions/test/browser/browser_ext_openPanel.js
index ed96bf2520..c56c0a31b6 100644
--- a/browser/components/extensions/test/browser/browser_ext_openPanel.js
+++ b/browser/components/extensions/test/browser/browser_ext_openPanel.js
@@ -136,16 +136,16 @@ add_task(async function test_openPopup_requires_user_interaction() {
{},
gBrowser.selectedBrowser
);
- await TestUtils.waitForCondition(() => !SidebarUI.isOpen);
+ await TestUtils.waitForCondition(() => !SidebarController.isOpen);
await click("#toggleSidebarAction");
- await TestUtils.waitForCondition(() => SidebarUI.isOpen);
+ await TestUtils.waitForCondition(() => SidebarController.isOpen);
await BrowserTestUtils.synthesizeMouseAtCenter(
"#toggleSidebarAction",
{},
gBrowser.selectedBrowser
);
- await TestUtils.waitForCondition(() => !SidebarUI.isOpen);
+ await TestUtils.waitForCondition(() => !SidebarController.isOpen);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
await extension.unload();
diff --git a/browser/components/extensions/test/browser/browser_ext_originControls.js b/browser/components/extensions/test/browser/browser_ext_originControls.js
index 176eef08bc..6bcae3ef8f 100644
--- a/browser/components/extensions/test/browser/browser_ext_originControls.js
+++ b/browser/components/extensions/test/browser/browser_ext_originControls.js
@@ -20,6 +20,12 @@ const l10n = new Localization(
true
);
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.originControls.grantByDefault", false]],
+ });
+});
+
async function makeExtension({
useAddonManager = "temporary",
manifest_version = 3,
diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
index fd589acdbd..71f13a4691 100644
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_show_matches.js
@@ -268,10 +268,14 @@ add_task(async function test_pageAction_restrictScheme_false() {
},
},
background: function () {
- browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
+ let articleReady = Promise.withResolvers();
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.url && changeInfo.url.startsWith("about:reader")) {
browser.test.sendMessage("readerModeEntered");
}
+ if (tab.isArticle && tab.url.includes("/readerModeArticle.html")) {
+ articleReady.resolve();
+ }
});
browser.test.onMessage.addListener(async msg => {
@@ -280,8 +284,20 @@ add_task(async function test_pageAction_restrictScheme_false() {
return;
}
+ browser.test.log("Waiting for tab.isArticle to be true");
+ await articleReady.promise;
+ browser.test.log("Toggling reader mode");
browser.tabs.toggleReaderMode();
});
+
+ browser.tabs.query(
+ { url: "*://example.com/*/readerModeArticle.html" },
+ tabs => {
+ if (tabs[0].isArticle) {
+ articleReady.resolve();
+ }
+ }
+ );
},
});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js b/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js
index fa2c414047..bdc5145dc5 100644
--- a/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js
+++ b/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js
@@ -70,6 +70,8 @@ add_task(async function testPopupSelectPopup() {
});
const selectRect = await SpecialPowers.spawn(iframe, [], async () => {
+ await SpecialPowers.contentTransformsReceived(content.window);
+
await ContentTaskUtils.waitForCondition(() => {
return content.document.querySelector("select");
});
diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_getContexts.js b/browser/components/extensions/test/browser/browser_ext_runtime_getContexts.js
new file mode 100644
index 0000000000..4d28cd5f87
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_getContexts.js
@@ -0,0 +1,597 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadTestSubscript("head_devtools.js");
+
+async function genericChecker() {
+ const params = new URLSearchParams(window.location.search);
+ const kind = params.get("kind");
+
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ if (msg == `${kind}-get-contexts-invalid-params`) {
+ browser.test.assertThrows(
+ () => browser.runtime.getContexts({ unknownParamName: true }),
+ /Type error for parameter filter \(Unexpected property "unknownParamName"\)/,
+ "Got the expected error on unexpected filter property"
+ );
+ browser.test.sendMessage(`${msg}:done`);
+ } else if (msg == `${kind}-get-contexts`) {
+ const filter = args[0];
+ try {
+ const result = await browser.runtime.getContexts(filter);
+ browser.test.sendMessage(`${msg}:result`, result);
+ } catch (err) {
+ // In case of unexpected errors, log a failure and let the test
+ // to continue to avoid it to only fail after timing out.
+ browser.test.fail(`browser.runtime.getContexts call rejected: ${err}`);
+ browser.test.sendMessage(`${msg}:result`, []);
+ }
+ } else if (msg == `${kind}-history-push-state`) {
+ const pushStateURL = args[0];
+ window.history.pushState({}, "", pushStateURL);
+ browser.test.sendMessage(`${msg}:done`);
+ } else if (msg == `${kind}-create-iframe`) {
+ const iframeUrl = args[0];
+ const iframe = document.createElement("iframe");
+ iframe.src = iframeUrl;
+ document.body.appendChild(iframe);
+ } else if (msg == `${kind}-open-options-page`) {
+ browser.runtime.openOptionsPage();
+ }
+ });
+
+ if (kind === "devtools-page") {
+ await browser.devtools.panels.create(
+ "Test DevTool Panel",
+ "fake-icon.png",
+ "page.html?kind=devtools-panel"
+ );
+ }
+
+ browser.test.log(`${kind} extension page loaded`);
+ browser.test.sendMessage(`${kind}-loaded`);
+}
+
+async function triggerActionPopup(extension, win, callback) {
+ // Window needs focus to open popups.
+ await focusWindow(win);
+ await clickBrowserAction(extension, win);
+ let browser = await awaitExtensionPanel(extension, win);
+
+ await callback();
+
+ let { unloadPromise } = await promiseBrowserContentUnloaded(browser);
+ closeBrowserAction(extension, win);
+ await unloadPromise;
+}
+
+const byWindowId = (a, b) => a.windowId - b.windowId;
+const byTabId = (a, b) => a.tabId - b.tabId;
+const byFrameId = (a, b) => a.frameId - b.frameId;
+const byContextType = (a, b) => a.contextType.localeCompare(b.contextType);
+
+const assertValidContextId = contextId => {
+ Assert.equal(
+ typeof contextId,
+ "string",
+ "contextId should be set to a string"
+ );
+ Assert.notEqual(
+ contextId.length,
+ 0,
+ "contextId should be set to a non-zero length string"
+ );
+};
+
+const assertGetContextsResult = (
+ actual,
+ expected,
+ msg,
+ { assertContextId = false } = {}
+) => {
+ const actualCopy = assertContextId ? actual : actual.map(it => ({ ...it }));
+ if (!assertContextId) {
+ actualCopy.forEach(it => delete it.contextId);
+ }
+ for (let [idx, expectedProps] of expected.entries()) {
+ Assert.deepEqual(actualCopy[idx], expectedProps, msg);
+ }
+ Assert.equal(
+ actualCopy.length,
+ expected.length,
+ "Got the expected number of extension contexts"
+ );
+};
+
+add_task(async function test_runtime_getContexts() {
+ const EXT_ID = "runtime-getContexts@mochitest";
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary", // To automatically show sidebar on load.
+ incognitoOverride: "spanning",
+ manifest: {
+ manifest_version: 3,
+ browser_specific_settings: { gecko: { id: EXT_ID } },
+
+ action: {
+ default_popup: "page.html?kind=action",
+ default_area: "navbar",
+ },
+
+ sidebar_action: {
+ default_panel: "page.html?kind=sidebar",
+ },
+
+ options_ui: {
+ page: "page.html?kind=options",
+ },
+
+ devtools_page: "page.html?kind=devtools-page",
+
+ background: {
+ page: "page.html?kind=background",
+ },
+ },
+
+ files: {
+ "page.html": `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"></head>
+ <body>
+ <script src="page.js"></script>
+ </body></html>
+ `,
+
+ "page.js": genericChecker,
+ },
+ });
+
+ const {
+ Management: {
+ global: { tabTracker, windowTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let firstWin = window;
+ let secondWin = await BrowserTestUtils.openNewBrowserWindow();
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-loaded");
+
+ // Expect 3 sidebars (2 non-private and 1 private windows).
+ await extension.awaitMessage("sidebar-loaded");
+ await extension.awaitMessage("sidebar-loaded");
+ await extension.awaitMessage("sidebar-loaded");
+
+ let firstWinId = windowTracker.getId(firstWin);
+ let secondWinId = windowTracker.getId(secondWin);
+ let privateWinId = windowTracker.getId(privateWin);
+
+ const getGetContextsResults = async ({ filter, sortBy }) => {
+ extension.sendMessage("background-get-contexts", filter);
+ let results = await extension.awaitMessage(
+ "background-get-contexts:result"
+ );
+ if (sortBy) {
+ results.sort(sortBy);
+ }
+ return results;
+ };
+
+ const resolveExtPageUrl = urlPath =>
+ WebExtensionPolicy.getByID(EXT_ID).extension.baseURI.resolve(urlPath);
+
+ const documentOrigin = resolveExtPageUrl("/").slice(0, -1);
+
+ const getExpectedExtensionContext = ({
+ contextId,
+ contextType,
+ documentUrl,
+ incognito = false,
+ frameId = 0,
+ tabId = -1,
+ windowId = -1,
+ }) => {
+ let props = {
+ contextType,
+ documentOrigin,
+ documentUrl,
+ incognito,
+ frameId,
+ tabId,
+ windowId,
+ };
+ if (contextId) {
+ props.contextId = contextId;
+ }
+ return props;
+ };
+
+ let expected = [
+ getExpectedExtensionContext({
+ contextType: "BACKGROUND",
+ documentUrl: resolveExtPageUrl("page.html?kind=background"),
+ }),
+
+ getExpectedExtensionContext({
+ contextType: "SIDE_PANEL",
+ documentUrl: resolveExtPageUrl("page.html?kind=sidebar"),
+ windowId: firstWinId,
+ }),
+
+ getExpectedExtensionContext({
+ contextType: "SIDE_PANEL",
+ documentUrl: resolveExtPageUrl("page.html?kind=sidebar"),
+ windowId: secondWinId,
+ }),
+
+ getExpectedExtensionContext({
+ contextType: "SIDE_PANEL",
+ documentUrl: resolveExtPageUrl("page.html?kind=sidebar"),
+ windowId: privateWinId,
+ incognito: true,
+ }),
+ ].sort(byWindowId);
+
+ info("Test getContexts error on unsupported getContexts filter property");
+ extension.sendMessage("background-get-contexts-invalid-params");
+ await extension.awaitMessage("background-get-contexts-invalid-params:done");
+
+ info("Test getContexts with a valid empty filter");
+ let actual = await getGetContextsResults({ filter: {}, sortBy: byWindowId });
+
+ assertGetContextsResult(
+ actual,
+ expected,
+ "Got the expected results from runtime.getContexts (with an empty filter)"
+ );
+
+ for (const ctx of actual) {
+ info(`Validate contextId for context ${ctx.contextType} ${ctx.contextId}`);
+ assertValidContextId(ctx.contextId);
+ }
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: secondWin.gBrowser,
+ url: resolveExtPageUrl("page.html?kind=tab"),
+ },
+ async browser => {
+ info("Wait the extension page to be fully loaded in the new tab");
+ await extension.awaitMessage("tab-loaded");
+
+ const tabId = tabTracker.getBrowserData(browser).tabId;
+
+ const expectedTabContext = getExpectedExtensionContext({
+ contextType: "TAB",
+ documentUrl: resolveExtPageUrl("page.html?kind=tab"),
+ windowId: secondWinId,
+ tabId,
+ incognito: false,
+ });
+
+ info("Test getContexts with contextTypes TAB filter");
+ let actual = await getGetContextsResults({
+ filter: { contextTypes: ["TAB"] },
+ });
+ assertGetContextsResult(
+ actual,
+ [expectedTabContext],
+ "Got the expected results from runtime.getContexts (with contextTypes TAB filter)"
+ );
+ assertValidContextId(actual[0].contextId);
+ const initialTabContextId = actual[0].contextId;
+
+ info("Test getContexts with contextTypes TabIds filter");
+ actual = await getGetContextsResults({
+ filter: { tabIds: [tabId] },
+ });
+ assertGetContextsResult(
+ actual,
+ [expectedTabContext],
+ "Got the expected results from runtime.getContexts (with tabIds filter)"
+ );
+
+ info("Test getContexts with contextTypes WindowIds filter");
+ actual = await getGetContextsResults({
+ filter: { windowIds: [secondWinId] },
+ sortBy: byTabId,
+ });
+ assertGetContextsResult(
+ actual,
+ [
+ expectedTabContext,
+ expected.find(it => it.windowId === secondWinId),
+ ].sort(byTabId),
+ "Got the expected results from runtime.getContexts (with windowIds filter)"
+ );
+
+ info("Test getContexts after navigating the tab");
+ const newTabURL = resolveExtPageUrl("page.html?kind=tab&navigated=true");
+ browser.loadURI(Services.io.newURI(newTabURL), {
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ await extension.awaitMessage("tab-loaded");
+
+ actual = await getGetContextsResults({
+ filter: {
+ contextTypes: ["TAB"],
+ windowIds: [secondWinId],
+ },
+ });
+ Assert.equal(actual.length, 1, "Expect 1 tab extension context");
+ Assert.equal(
+ actual[0].documentUrl,
+ newTabURL,
+ "Expect documentUrl to match the new loaded url"
+ );
+ Assert.equal(actual[0].frameId, 0, "Got expected frameId");
+ Assert.equal(
+ actual[0].tabId,
+ expectedTabContext.tabId,
+ "Got expected tabId"
+ );
+ Assert.notEqual(
+ actual[0].contextId,
+ initialTabContextId,
+ "Expect contextId to change on navigated tab"
+ );
+ }
+ );
+
+ await triggerActionPopup(extension, privateWin, async () => {
+ info("Wait the extension page to be fully loaded in the action popup");
+ await extension.awaitMessage("action-loaded");
+
+ const expectedPopupContext = getExpectedExtensionContext({
+ contextType: "POPUP",
+ documentUrl: resolveExtPageUrl("page.html?kind=action"),
+ windowId: privateWinId,
+ tabId: -1,
+ incognito: true,
+ });
+
+ info("Test getContexts with contextTypes POPUP filter");
+ let actual = await getGetContextsResults({
+ filter: {
+ contextTypes: ["POPUP"],
+ },
+ });
+ assertGetContextsResult(
+ actual,
+ [expectedPopupContext],
+ "Got the expected results from runtime.getContexts (with contextTypes POPUP filter)"
+ );
+
+ info("Test getContexts with incognito true filter");
+ actual = await getGetContextsResults({
+ filter: { incognito: true },
+ sortBy: byContextType,
+ });
+ assertGetContextsResult(
+ actual.sort(byContextType),
+ [expectedPopupContext, ...expected.filter(it => it.incognito)].sort(
+ byContextType
+ ),
+ "Got the expected results from runtime.getContexts (with contextTypes incognito true filter)"
+ );
+ });
+
+ info("Test getContexts with existing background iframes");
+ extension.sendMessage(
+ `background-create-iframe`,
+ resolveExtPageUrl("page.html?kind=background-subframe")
+ );
+ await extension.awaitMessage(`background-subframe-loaded`);
+
+ actual = await getGetContextsResults({
+ filter: { contextTypes: ["BACKGROUND"] },
+ });
+
+ Assert.equal(
+ actual.length,
+ 2,
+ "Expect 2 background extension contexts to be found"
+ );
+ const bgTopFrame = actual.find(
+ it => it.documentUrl === resolveExtPageUrl("page.html?kind=background")
+ );
+ const bgSubFrame = actual.find(
+ it =>
+ it.documentUrl === resolveExtPageUrl("page.html?kind=background-subframe")
+ );
+
+ assertValidContextId(bgTopFrame.contextId);
+ assertValidContextId(bgSubFrame.contextId);
+ Assert.notEqual(
+ bgTopFrame.contextId,
+ bgSubFrame.contextId,
+ "Expect background top and sub frame to have different contextIds"
+ );
+
+ Assert.equal(
+ bgTopFrame.frameId,
+ 0,
+ "Expect background top frame to have frameId 0"
+ );
+ ok(
+ typeof bgSubFrame.frameId === "number" && bgSubFrame.frameId > 0,
+ "Expect background sub frame to have a non zero frameId"
+ );
+ Assert.equal(
+ bgSubFrame.windowId,
+ bgSubFrame.windowId,
+ "Expect background top frame to have same windowId as the top frame"
+ );
+ Assert.equal(
+ bgSubFrame.tabId,
+ bgTopFrame.tabId,
+ "Expect background top frame to have same tabId as the top frame"
+ );
+
+ info("Test getContexts with existing sidebars iframes");
+ extension.sendMessage(
+ `sidebar-create-iframe`,
+ resolveExtPageUrl("page.html?kind=sidebar-subframe")
+ );
+ // Expect 3 sidebar subframe to be created.
+ await extension.awaitMessage(`sidebar-subframe-loaded`);
+ await extension.awaitMessage(`sidebar-subframe-loaded`);
+ await extension.awaitMessage(`sidebar-subframe-loaded`);
+
+ actual = await getGetContextsResults({
+ filter: { contextTypes: ["SIDE_PANEL"], windowIds: [firstWinId] },
+ sortBy: byFrameId,
+ });
+ Assert.equal(
+ actual.length,
+ 2,
+ "Expect 2 sidebar extension contexts to be found for the first window"
+ );
+ // NOTE: we have already asserted that the sidebar top level frame has frameId 0.
+ Assert.greater(
+ actual.find(
+ it =>
+ it.documentUrl == resolveExtPageUrl("page.html?kind=sidebar-subframe")
+ )?.frameId,
+ 0,
+ "Expect sidebar subframe to have the expected frameId"
+ );
+ // NOTE: we have already asserted that the top level frame have tabId -1.
+ Assert.equal(
+ actual[0].tabId,
+ actual[1].tabId,
+ "Expect iframe and top level sidebar frame to have the same tabId"
+ );
+
+ actual = await getGetContextsResults({
+ filter: { contextTypes: ["SIDE_PANEL"], windowIds: [secondWinId] },
+ sortBy: byFrameId,
+ });
+ Assert.equal(
+ actual.length,
+ 2,
+ "Expect 2 sidebar extension contexts to be found for the second window"
+ );
+
+ actual = await getGetContextsResults({
+ filter: { contextTypes: ["SIDE_PANEL"], incognito: true },
+ });
+ Assert.equal(
+ actual.length,
+ 2,
+ "Expect 2 sidebar extension contexts to be found for private windows"
+ );
+
+ info("Test getContexts after background history push state");
+ let pushStateURLPath = "/page.html?kind=background&pushedState=1";
+ extension.sendMessage("background-history-push-state", pushStateURLPath);
+ await extension.awaitMessage("background-history-push-state:done");
+
+ actual = await getGetContextsResults({
+ filter: { contextTypes: ["BACKGROUND"], frameIds: [0] },
+ });
+ Assert.equal(
+ actual.length,
+ 1,
+ "Expect 1 top level background context to be found"
+ );
+ Assert.equal(
+ actual[0].contextId,
+ bgTopFrame.contextId,
+ "Expect top level background contextId to NOT be changed"
+ );
+ Assert.equal(
+ actual[0].documentUrl,
+ resolveExtPageUrl(pushStateURLPath),
+ "Expect top level background documentUrl to change due to history.pushState"
+ );
+
+ await BrowserTestUtils.closeWindow(privateWin);
+ await BrowserTestUtils.closeWindow(secondWin);
+
+ info(
+ "Test getContexts after opening an options page embedded in an about:addons tab"
+ );
+ await BrowserTestUtils.withNewTab("about:addons", async () => {
+ extension.sendMessage("background-open-options-page");
+ await extension.awaitMessage("options-loaded");
+ const { selectedBrowser } = firstWin.gBrowser;
+ Assert.equal(
+ selectedBrowser.currentURI.spec,
+ "about:addons",
+ "Expect an about:addons tab to be current active tab"
+ );
+ let optionsTabId = tabTracker.getBrowserData(selectedBrowser).tabId;
+
+ actual = await getGetContextsResults({
+ filter: { windowIds: [firstWinId], tabIds: [optionsTabId] },
+ });
+ assertGetContextsResult(
+ actual,
+ [
+ getExpectedExtensionContext({
+ contextType: "TAB",
+ documentUrl: resolveExtPageUrl("page.html?kind=options"),
+ windowId: firstWinId,
+ tabId: optionsTabId,
+ }),
+ ],
+ "Got the expected results from runtime.getContexts for an options_page"
+ );
+ });
+
+ info("Test getContexts with an extension devtools page and devtools panel");
+ await BrowserTestUtils.withNewTab("https://example.com", async () => {
+ const tab = gBrowser.selectedTab;
+ const toolbox = await openToolboxForTab(tab);
+
+ info("Wait for the devtools page to be loaded");
+ await extension.awaitMessage("devtools-page-loaded");
+
+ Assert.equal(
+ toolbox.getAdditionalTools()?.length,
+ 1,
+ "Expecte extension devtools panel to be registered"
+ );
+ let panelId = toolbox.getAdditionalTools()[0].id;
+ await gDevTools.showToolboxForTab(tab, { toolId: panelId });
+
+ info("Wait for the devtools panel to be loaded");
+ await extension.awaitMessage("devtools-panel-loaded");
+
+ actual = await getGetContextsResults({ filter: {} });
+ // Expect the backgrond page and its subframe to still be returned.
+ Assert.equal(
+ actual.filter(ctx => ctx.contextType === "BACKGROUND").length,
+ 2,
+ "Expect the existing 2 background context types"
+ );
+ // Expect the side_panel page and its subframe to still be returned.
+ Assert.equal(
+ actual.filter(ctx => ctx.contextType === "SIDE_PANEL").length,
+ 2,
+ "Expect the existing 2 side_panel context types"
+ );
+ // Expect no other context to be listed in the getContexts results
+ // (devtools page and panel are currently expected to not be
+ // part of getContexts results).
+ Assert.deepEqual(
+ actual.filter(
+ ctx => !["BACKGROUND", "SIDE_PANEL"].includes(ctx.contextType)
+ ),
+ [],
+ "DevTools page and panel are not listed in getContexts results"
+ );
+
+ await closeToolboxForTab(gBrowser.selectedTab);
+ });
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js
index 7dd41ecfe9..715cf14458 100644
--- a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js
@@ -7,7 +7,7 @@ requestLongerTimeout(2);
loadTestSubscript("head_sessions.js");
add_task(async function test_sessions_get_recently_closed() {
- async function openAndCloseWindow(url = "http://example.com", tabUrls) {
+ async function openAndCloseWindow(url = "https://example.com", tabUrls) {
let win = await BrowserTestUtils.openNewBrowserWindow();
BrowserTestUtils.startLoadingURIString(win.gBrowser.selectedBrowser, url);
await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
@@ -77,13 +77,13 @@ add_task(async function test_sessions_get_recently_closed() {
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
- "http://example.com"
+ "https://example.com"
);
BrowserTestUtils.removeTab(tab);
tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
- "http://example.com"
+ "https://example.com"
);
BrowserTestUtils.removeTab(tab);
@@ -123,7 +123,7 @@ add_task(async function test_sessions_get_recently_closed_navigated() {
.then(recentlyClosed => {
let tab = recentlyClosed[0].window.tabs[0];
browser.test.assertEq(
- "http://example.com/",
+ "https://example.com/",
tab.url,
"Tab in closed window has the expected url."
);
@@ -144,7 +144,7 @@ add_task(async function test_sessions_get_recently_closed_navigated() {
// Test with a window with navigation history.
let win = await BrowserTestUtils.openNewBrowserWindow();
- for (let url of ["about:robots", "about:mozilla", "http://example.com/"]) {
+ for (let url of ["about:robots", "about:mozilla", "https://example.com/"]) {
BrowserTestUtils.startLoadingURIString(win.gBrowser.selectedBrowser, url);
await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
}
@@ -178,7 +178,7 @@ add_task(
"The second tab with empty.xpi has no url field due to empty history."
);
browser.test.assertEq(
- "http://example.com/",
+ "https://example.com/",
win.tabs[2].url,
"The third tab is example.com."
);
@@ -195,7 +195,7 @@ add_task(
// Test with a window with empty history.
let xpi =
- "http://example.com/browser/browser/components/extensions/test/browser/empty.xpi";
+ "https://example.com/browser/browser/components/extensions/test/browser/empty.xpi";
let newWin = await BrowserTestUtils.openNewBrowserWindow();
await BrowserTestUtils.openNewForegroundTab({
gBrowser: newWin.gBrowser,
@@ -205,7 +205,7 @@ add_task(
});
await BrowserTestUtils.openNewForegroundTab({
gBrowser: newWin.gBrowser,
- url: "http://example.com/",
+ url: "https://example.com/",
});
await BrowserTestUtils.closeWindow(newWin);
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js b/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js
index c89e9d39dc..46a925039a 100644
--- a/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js
@@ -15,7 +15,7 @@ ChromeUtils.defineESModuleGetters(this, {
// Check that we can restore a tab modified by an extension.
add_task(async function test_restoringModifiedTab() {
function background() {
- browser.tabs.create({ url: "http://example.com/" });
+ browser.tabs.create({ url: "https://example.com/" });
browser.test.onMessage.addListener(msg => {
if (msg == "change-tab") {
browser.tabs.executeScript({ code: 'location.href += "?changedTab";' });
@@ -32,14 +32,14 @@ add_task(async function test_restoringModifiedTab() {
background,
});
- const contentScriptTabURL = "http://example.com/?changedTab";
+ const contentScriptTabURL = "https://example.com/?changedTab";
let win = await BrowserTestUtils.openNewBrowserWindow({});
// Open and close a tabs.
let tabPromise = BrowserTestUtils.waitForNewTab(
win.gBrowser,
- "http://example.com/",
+ "https://example.com/",
true
);
await extension.startup();
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
index 8498f73071..ac2d19cf23 100644
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
@@ -7,6 +7,7 @@ requestLongerTimeout(2);
let extData = {
manifest: {
sidebar_action: {
+ default_icon: "default.png",
default_panel: "sidebar.html",
},
},
@@ -29,6 +30,9 @@ let extData = {
browser.test.sendMessage("sidebar");
};
},
+
+ "default.png": imageBuffer,
+ "1.png": imageBuffer,
},
background: function () {
@@ -44,6 +48,8 @@ let extData = {
let { arg = {}, result } = data;
let isOpen = await browser.sidebarAction.isOpen(arg);
browser.test.assertEq(result, isOpen, "expected value from isOpen");
+ } else if (msg === "set-icon") {
+ await browser.sidebarAction.setIcon({ path: data });
}
browser.test.sendMessage("done");
});
@@ -148,7 +154,7 @@ add_task(async function sidebar_isOpen() {
info("Test extension1's sidebar is opened on install");
await extension1.awaitMessage("sidebar");
await sendMessage(extension1, "isOpen", { result: true });
- let sidebar1ID = SidebarUI.currentID;
+ let sidebar1ID = SidebarController.currentID;
info("Load extension2");
let extension2 = ExtensionTestUtils.loadExtension(getExtData());
@@ -160,7 +166,7 @@ add_task(async function sidebar_isOpen() {
await sendMessage(extension2, "isOpen", { result: true });
info("Switch back to extension1's sidebar");
- SidebarUI.show(sidebar1ID);
+ SidebarController.show(sidebar1ID);
await extension1.awaitMessage("sidebar");
await sendMessage(extension1, "isOpen", { result: true });
await sendMessage(extension2, "isOpen", { result: false });
@@ -195,7 +201,7 @@ add_task(async function sidebar_isOpen() {
newWin.close();
info("Close the sidebar in the original window");
- SidebarUI.hide();
+ SidebarController.hide();
await sendMessage(extension1, "isOpen", { result: false });
await sendMessage(extension2, "isOpen", { result: false });
@@ -223,11 +229,15 @@ add_task(async function testShortcuts() {
async function toggleSwitcherPanel(win = window) {
// Open and close the switcher panel to trigger shortcut content rendering.
- let switcherPanelShown = promisePopupShown(win.SidebarUI._switcherPanel);
- win.SidebarUI.showSwitcherPanel();
+ let switcherPanelShown = promisePopupShown(
+ win.SidebarController._switcherPanel
+ );
+ win.SidebarController.showSwitcherPanel();
await switcherPanelShown;
- let switcherPanelHidden = promisePopupHidden(win.SidebarUI._switcherPanel);
- win.SidebarUI.hideSwitcherPanel();
+ let switcherPanelHidden = promisePopupHidden(
+ win.SidebarController._switcherPanel
+ );
+ win.SidebarController.hideSwitcherPanel();
await switcherPanelHidden;
}
@@ -301,3 +311,39 @@ add_task(async function testShortcuts() {
await BrowserTestUtils.closeWindow(win);
});
+
+add_task(async function sidebar_switcher_panel_icon_update() {
+ info("Load extension");
+ const extension = ExtensionTestUtils.loadExtension(getExtData());
+ await extension.startup();
+
+ info("Test extension's sidebar is opened on install");
+ await extension.awaitMessage("sidebar");
+ await sendMessage(extension, "isOpen", { result: true });
+ const sidebarID = SidebarController.currentID;
+
+ const item = SidebarController._switcherPanel.querySelector(
+ ".webextension-menuitem"
+ );
+ let iconUrl = `moz-extension://${extension.uuid}/default.png`;
+ is(
+ item.style.getPropertyValue("--webextension-menuitem-image"),
+ `image-set(url("${iconUrl}"), url("${iconUrl}") 2x)`,
+ "Extension has the correct icon."
+ );
+ SidebarController.hide();
+ await sendMessage(extension, "isOpen", { result: false });
+
+ await sendMessage(extension, "set-icon", "1.png");
+ await SidebarController.show(sidebarID);
+ await extension.awaitMessage("sidebar");
+ await sendMessage(extension, "isOpen", { result: true });
+ iconUrl = `moz-extension://${extension.uuid}/1.png`;
+ is(
+ item.style.getPropertyValue("--webextension-menuitem-image"),
+ `image-set(url("${iconUrl}"), url("${iconUrl}") 2x)`,
+ "Extension has updated icon."
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js
index 621d2d1180..a82f6b8186 100644
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js
@@ -54,7 +54,7 @@ add_task(async function test_sidebar_click_isAppTab_behavior() {
await extension.awaitMessage("sidebar-ready");
// This test fails if docShell.isAppTab has not been set to true.
- let content = SidebarUI.browser.contentWindow;
+ let content = SidebarController.browser.contentWindow;
// Wait for the layout to be flushed, otherwise this test may
// fail intermittently if synthesizeMouseAtCenter is being called
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
index 7057037a5e..93d34d4487 100644
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
@@ -124,7 +124,7 @@ async function runTests(options) {
});
// Wait for initial sidebar load.
- SidebarUI.browser.addEventListener(
+ SidebarController.browser.addEventListener(
"load",
async () => {
// Wait for the background page listeners to be ready and
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js
index d50d96b822..f8ab238148 100644
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js
@@ -42,7 +42,7 @@ add_task(async function sidebar_httpAuthPrompt() {
// Wait for the http auth prompt and close it with accept button.
let promptPromise = PromptTestUtils.handleNextPrompt(
- SidebarUI.browser.contentWindow,
+ SidebarController.browser.contentWindow,
{
modalType: Services.prompt.MODAL_TYPE_WINDOW,
promptType: "promptUserAndPass",
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js
index 221447cf2e..39269d9d06 100644
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js
@@ -124,11 +124,14 @@ add_task(async function test_sidebarAction_not_allowed() {
await extension.startup();
let sidebarID = `${makeWidgetId(extension.id)}-sidebar-action`;
- ok(SidebarUI.sidebars.has(sidebarID), "sidebar exists in non-private window");
+ ok(
+ SidebarController.sidebars.has(sidebarID),
+ "sidebar exists in non-private window"
+ );
let winData = await getIncognitoWindow();
- let hasSidebar = winData.win.SidebarUI.sidebars.has(sidebarID);
+ let hasSidebar = winData.win.SidebarController.sidebars.has(sidebarID);
ok(!hasSidebar, "sidebar does not exist in private window");
// Test API access to private window data.
extension.sendMessage(winData.details);
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js
index 55c83ee0b1..8ac1dd76a1 100644
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js
@@ -52,9 +52,10 @@ add_task(async function test_sidebar_disconnect() {
await connected;
// Bug 1445080 fixes currentURI, test to avoid future breakage.
- let currentURI = window.SidebarUI.browser.contentDocument.getElementById(
- "webext-panels-browser"
- ).currentURI;
+ let currentURI =
+ window.SidebarController.browser.contentDocument.getElementById(
+ "webext-panels-browser"
+ ).currentURI;
is(currentURI.scheme, "moz-extension", "currentURI is set correctly");
// switching sidebar to another extension
@@ -68,7 +69,7 @@ add_task(async function test_sidebar_disconnect() {
// switching sidebar to built-in sidebar
let disconnected = extension2.awaitMessage("disconnected");
- window.SidebarUI.show("viewBookmarksSidebar");
+ window.SidebarController.show("viewBookmarksSidebar");
await disconnected;
await extension.unload();
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js
index 58f2b07797..c8f3f043ec 100644
--- a/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js
@@ -49,7 +49,7 @@ add_task(async function sidebar_windows() {
let secondSidebar = extension.awaitMessage("sidebar");
- // SidebarUI relies on window.opener being set, which is normal behavior when
+ // SidebarController relies on window.opener being set, which is normal behavior when
// using menu or key commands to open a new browser window.
let win = await BrowserTestUtils.openNewBrowserWindow();
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..b83d4c2e23 100644
--- a/browser/components/extensions/test/browser/browser_unified_extensions.js
+++ b/browser/components/extensions/test/browser/browser_unified_extensions.js
@@ -44,6 +44,9 @@ add_setup(async function () {
// panel, which could happen when a previous test file resizes the current
// window.
await ensureMaximizedWindow(window);
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.originControls.grantByDefault", false]],
+ });
});
add_task(async function test_button_enabled_by_pref() {
@@ -1222,7 +1225,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/extensions/test/browser/browser_unified_extensions_accessibility.js b/browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js
index 44d861d97c..8439cf30dd 100644
--- a/browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js
@@ -5,6 +5,12 @@
loadTestSubscript("head_unified_extensions.js");
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.originControls.grantByDefault", false]],
+ });
+});
+
add_task(async function test_keyboard_navigation_activeScript() {
const extension1 = ExtensionTestUtils.loadExtension({
manifest: {
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js b/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js
index d04d85e535..a7bdc8273c 100644
--- a/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js
@@ -5,10 +5,6 @@
requestLongerTimeout(2);
-ChromeUtils.defineESModuleGetters(this, {
- AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
-});
-
const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
"resource://testing-common/EnterprisePolicyTesting.sys.mjs"
);
@@ -31,21 +27,6 @@ const promiseExtensionUninstalled = extensionId => {
});
};
-function waitClosedWindow(win) {
- return new Promise(resolve => {
- function onWindowClosed() {
- if (win && !win.closed) {
- // If a specific window reference has been passed, then check
- // that the window is closed before resolving the promise.
- return;
- }
- Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
- resolve();
- }
- Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
- });
-}
-
function assertVisibleContextMenuItems(contextMenu, expected) {
let visibleItems = contextMenu.querySelectorAll(
":is(menuitem, menuseparator):not([hidden])"
@@ -366,90 +347,33 @@ add_task(async function test_report_extension() {
// closed and about:addons is open with the "abuse report dialog".
const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
- if (AbuseReporter.amoFormEnabled) {
- const reportURL = Services.urlFormatter
- .formatURLPref("extensions.abuseReport.amoFormURL")
- .replace("%addonID%", extension.id);
-
- const promiseReportTab = BrowserTestUtils.waitForNewTab(
- gBrowser,
- reportURL,
- /* waitForLoad */ false,
- // Do not expect it to be the next tab opened
- /* waitForAnyTab */ true
- );
- contextMenu.activateItem(reportButton);
- const [reportTab] = await Promise.all([promiseReportTab, hidden]);
- // Remove the report tab and expect the selected tab
- // to become the about:addons tab.
- BrowserTestUtils.removeTab(reportTab);
- if (AbuseReporter.amoFormEnabled) {
- is(
- gBrowser.selectedBrowser.currentURI.spec,
- "about:blank",
- "Expect about:addons tab to have not been opened (amoFormEnabled=true)"
- );
- } else {
- is(
- gBrowser.selectedBrowser.currentURI.spec,
- "about:addons",
- "Got about:addons tab selected (amoFormEnabled=false)"
- );
- }
- return;
- }
+ const reportURL = Services.urlFormatter
+ .formatURLPref("extensions.abuseReport.amoFormURL")
+ .replace("%addonID%", extension.id);
- const abuseReportOpen = BrowserTestUtils.waitForCondition(
- () => AbuseReporter.getOpenDialog(),
- "wait for the abuse report dialog to have been opened"
+ const promiseReportTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ reportURL,
+ /* waitForLoad */ false,
+ // Do not expect it to be the next tab opened
+ /* waitForAnyTab */ true
);
contextMenu.activateItem(reportButton);
- const [reportDialogWindow] = await Promise.all([abuseReportOpen, hidden]);
-
- const reportDialogParams =
- reportDialogWindow.arguments[0].wrappedJSObject;
- is(
- reportDialogParams.report.addon.id,
- extension.id,
- "abuse report dialog has the expected addon id"
- );
+ const [reportTab] = await Promise.all([promiseReportTab, hidden]);
+ // Remove the report tab and expect the selected tab
+ // to become the about:addons tab.
+ BrowserTestUtils.removeTab(reportTab);
is(
- reportDialogParams.report.reportEntryPoint,
- "unified_context_menu",
- "abuse report dialog has the expected reportEntryPoint"
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:blank",
+ "Expect about:addons tab to have not been opened"
);
-
- let promiseClosedWindow = waitClosedWindow();
- reportDialogWindow.close();
- // Wait for the report dialog window to be completely closed
- // (to prevent an intermittent failure due to a race between
- // the dialog window being closed and the test tasks that follows
- // opening the unified extensions button panel to not lose the
- // focus and be suddently closed before the task has done with
- // its assertions, see Bug 1782304).
- await promiseClosedWindow;
});
}
const [ext] = createExtensions([{ name: "an extension" }]);
await ext.startup();
-
- info("Test report with amoFormEnabled=true");
-
- await SpecialPowers.pushPrefEnv({
- set: [["extensions.abuseReport.amoFormEnabled", true]],
- });
await runReportTest(ext);
- await SpecialPowers.popPrefEnv();
-
- info("Test report with amoFormEnabled=false");
-
- await SpecialPowers.pushPrefEnv({
- set: [["extensions.abuseReport.amoFormEnabled", false]],
- });
- await runReportTest(ext);
- await SpecialPowers.popPrefEnv();
-
await ext.unload();
});
diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js
index 344eb3cca7..2d6835a034 100644
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -31,6 +31,7 @@
* loadTestSubscript awaitBrowserLoaded
* getScreenAt roundCssPixcel getCssAvailRect isRectContained
* getToolboxBackgroundColor
+ * promiseBrowserContentUnloaded
*/
// There are shutdown issues for which multiple rejections are left uncaught.
@@ -167,6 +168,44 @@ function promiseAnimationFrame(win = window) {
return AppUiTestInternals.promiseAnimationFrame(win);
}
+async function promiseBrowserContentUnloaded(browser) {
+ // Wait until the content has unloaded before resuming the test, to avoid
+ // calling extension.getViews too early (and having intermittent failures).
+ const MSG_WINDOW_DESTROYED = "Test:BrowserContentDestroyed";
+ let unloadPromise = new Promise(resolve => {
+ Services.ppmm.addMessageListener(MSG_WINDOW_DESTROYED, function listener() {
+ Services.ppmm.removeMessageListener(MSG_WINDOW_DESTROYED, listener);
+ resolve();
+ });
+ });
+
+ await ContentTask.spawn(
+ browser,
+ MSG_WINDOW_DESTROYED,
+ MSG_WINDOW_DESTROYED => {
+ let innerWindowId = this.content.windowGlobalChild.innerWindowId;
+ let observer = subject => {
+ if (
+ innerWindowId === subject.QueryInterface(Ci.nsISupportsPRUint64).data
+ ) {
+ Services.obs.removeObserver(observer, "inner-window-destroyed");
+
+ // Use process message manager to ensure that the message is delivered
+ // even after the <browser>'s message manager is disconnected.
+ Services.cpmm.sendAsyncMessage(MSG_WINDOW_DESTROYED);
+ }
+ };
+ // Observe inner-window-destroyed, like ExtensionPageChild, to ensure that
+ // the ExtensionPageContextChild instance has been unloaded when we resolve
+ // the unloadPromise.
+ Services.obs.addObserver(observer, "inner-window-destroyed");
+ }
+ );
+
+ // Return an object so that callers can use "await".
+ return { unloadPromise };
+}
+
function promisePopupHidden(popup) {
return new Promise(resolve => {
let onPopupHidden = () => {
@@ -437,10 +476,11 @@ async function openContextMenuInPopup(
}
async function openContextMenuInSidebar(selector = "body") {
- let contentAreaContextMenu = SidebarUI.browser.contentDocument.getElementById(
- "contentAreaContextMenu"
- );
- let browser = SidebarUI.browser.contentDocument.getElementById(
+ let contentAreaContextMenu =
+ SidebarController.browser.contentDocument.getElementById(
+ "contentAreaContextMenu"
+ );
+ let browser = SidebarController.browser.contentDocument.getElementById(
"webext-panels-browser"
);
let popupShownPromise = BrowserTestUtils.waitForEvent(
@@ -452,7 +492,9 @@ async function openContextMenuInSidebar(selector = "body") {
// fail intermittently if synthesizeMouseAtCenter is being called
// while the sidebar is still opening and the browser window layout
// being recomputed.
- await SidebarUI.browser.contentWindow.promiseDocumentFlushed(() => {});
+ await SidebarController.browser.contentWindow.promiseDocumentFlushed(
+ () => {}
+ );
info("Opening context menu in sidebarAction panel");
await BrowserTestUtils.synthesizeMouseAtCenter(
diff --git a/browser/components/firefoxview/HistoryController.sys.mjs b/browser/components/firefoxview/HistoryController.sys.mjs
new file mode 100644
index 0000000000..b6f316e8e7
--- /dev/null
+++ b/browser/components/firefoxview/HistoryController.sys.mjs
@@ -0,0 +1,383 @@
+/* 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 = {};
+
+import { getLogger } from "chrome://browser/content/firefoxview/helpers.mjs";
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PlacesQuery: "resource://gre/modules/PlacesQuery.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",
+ },
+};
+
+/**
+ * A list of visits displayed on a card.
+ *
+ * @typedef {object} CardEntry
+ *
+ * @property {string} domain
+ * @property {HistoryVisit[]} items
+ * @property {string} l10nId
+ */
+
+export class HistoryController {
+ /**
+ * @type {{ entries: CardEntry[]; searchQuery: string; sortOption: string; }}
+ */
+ historyCache;
+ host;
+ searchQuery;
+ sortOption;
+ #todaysDate;
+ #yesterdaysDate;
+
+ constructor(host, options) {
+ this.placesQuery = new lazy.PlacesQuery();
+ this.searchQuery = "";
+ this.sortOption = "date";
+ this.searchResultsLimit = options?.searchResultsLimit || 300;
+ this.component = HISTORY_MAP_L10N_IDS?.[options?.component]
+ ? options?.component
+ : "firefoxview";
+ this.historyCache = {
+ entries: [],
+ searchQuery: null,
+ sortOption: null,
+ };
+ this.host = host;
+
+ host.addController(this);
+ }
+
+ hostConnected() {
+ this.placesQuery.observeHistory(historyMap => this.updateCache(historyMap));
+ }
+
+ hostDisconnected() {
+ ChromeUtils.idleDispatch(() => this.placesQuery.close());
+ }
+
+ deleteFromHistory() {
+ lazy.PlacesUtils.history.remove(this.host.triggerNode.url);
+ }
+
+ onSearchQuery(e) {
+ this.searchQuery = e.detail.query;
+ this.updateCache();
+ }
+
+ onChangeSortOption(e) {
+ this.sortOption = e.target.value;
+ this.updateCache();
+ }
+
+ get historyVisits() {
+ return this.historyCache.entries;
+ }
+
+ get searchResults() {
+ return this.historyCache.searchQuery
+ ? this.historyCache.entries[0].items
+ : null;
+ }
+
+ get totalVisitsCount() {
+ return this.historyVisits.reduce(
+ (count, entry) => count + entry.items.length,
+ 0
+ );
+ }
+
+ get isHistoryEmpty() {
+ return !this.historyVisits.length;
+ }
+
+ /**
+ * Update cached history.
+ *
+ * @param {Map<CacheKey, HistoryVisit[]>} [historyMap]
+ * If provided, performs an update using the given data (instead of fetching
+ * it from the db).
+ */
+ async updateCache(historyMap) {
+ const { searchQuery, sortOption } = this;
+ const entries = searchQuery
+ ? await this.#getVisitsForSearchQuery(searchQuery)
+ : await this.#getVisitsForSortOption(sortOption, historyMap);
+ if (this.searchQuery !== searchQuery || this.sortOption !== sortOption) {
+ // This query is stale, discard results and do not update the cache / UI.
+ return;
+ }
+ for (const { items } of entries) {
+ for (const item of items) {
+ this.#normalizeVisit(item);
+ }
+ }
+ this.historyCache = { entries, searchQuery, sortOption };
+ this.host.requestUpdate();
+ }
+
+ /**
+ * Normalize data for fxview-tabs-list.
+ *
+ * @param {HistoryVisit} visit
+ * The visit to format.
+ */
+ #normalizeVisit(visit) {
+ visit.time = visit.date.getTime();
+ visit.title = visit.title || visit.url;
+ visit.icon = `page-icon:${visit.url}`;
+ visit.primaryL10nId = "fxviewtabrow-tabs-list-tab";
+ visit.primaryL10nArgs = JSON.stringify({
+ targetURI: visit.url,
+ });
+ visit.secondaryL10nId = "fxviewtabrow-options-menu-button";
+ visit.secondaryL10nArgs = JSON.stringify({
+ tabTitle: visit.title || visit.url,
+ });
+ }
+
+ async #getVisitsForSearchQuery(searchQuery) {
+ let items = [];
+ try {
+ items = await this.placesQuery.searchHistory(
+ searchQuery,
+ this.searchResultsLimit
+ );
+ } catch (e) {
+ getLogger("HistoryController").warn(
+ "There is a new search query in progress, so cancelling this one.",
+ e
+ );
+ }
+ return [{ items }];
+ }
+
+ async #getVisitsForSortOption(sortOption, historyMap) {
+ if (!historyMap) {
+ historyMap = await this.#fetchHistory();
+ }
+ switch (sortOption) {
+ case "date":
+ this.#setTodaysDate();
+ return this.#getVisitsForDate(historyMap);
+ case "site":
+ return this.#getVisitsForSite(historyMap);
+ default:
+ return [];
+ }
+ }
+
+ #setTodaysDate() {
+ const now = new Date();
+ this.#todaysDate = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate()
+ );
+ this.#yesterdaysDate = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate() - 1
+ );
+ }
+
+ /**
+ * Get a list of visits, sorted by date, in reverse chronological order.
+ *
+ * @param {Map<number, HistoryVisit[]>} historyMap
+ * @returns {CardEntry[]}
+ */
+ #getVisitsForDate(historyMap) {
+ const entries = [];
+ const visitsFromToday = this.#getVisitsFromToday(historyMap);
+ const visitsFromYesterday = this.#getVisitsFromYesterday(historyMap);
+ const visitsByDay = this.#getVisitsByDay(historyMap);
+ const visitsByMonth = this.#getVisitsByMonth(historyMap);
+
+ // Add visits from today and yesterday.
+ if (visitsFromToday.length) {
+ entries.push({
+ l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-today"],
+ items: visitsFromToday,
+ });
+ }
+ if (visitsFromYesterday.length) {
+ entries.push({
+ l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-yesterday"],
+ items: visitsFromYesterday,
+ });
+ }
+
+ // Add visits from this month, grouped by day.
+ visitsByDay.forEach(visits => {
+ entries.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 => {
+ entries.push({
+ l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-prev-month"],
+ items: visits,
+ });
+ });
+ return entries;
+ }
+
+ #getVisitsFromToday(cachedHistory) {
+ const mapKey = this.placesQuery.getStartOfDayTimestamp(this.#todaysDate);
+ const visits = cachedHistory.get(mapKey) ?? [];
+ return [...visits];
+ }
+
+ #getVisitsFromYesterday(cachedHistory) {
+ const mapKey = this.placesQuery.getStartOfDayTimestamp(
+ this.#yesterdaysDate
+ );
+ const visits = cachedHistory.get(mapKey) ?? [];
+ return [...visits];
+ }
+
+ /**
+ * Get a list of visits per day for each day on this month, excluding today
+ * and yesterday.
+ *
+ * @param {Map<number, HistoryVisit[]>} cachedHistory
+ * The history cache to process.
+ * @returns {HistoryVisit[][]}
+ * A list of visits for each day.
+ */
+ #getVisitsByDay(cachedHistory) {
+ const visitsPerDay = [];
+ for (const [time, visits] of cachedHistory.entries()) {
+ const date = new Date(time);
+ if (
+ this.#isSameDate(date, this.#todaysDate) ||
+ this.#isSameDate(date, this.#yesterdaysDate)
+ ) {
+ continue;
+ } else if (!this.#isSameMonth(date, this.#todaysDate)) {
+ break;
+ } else {
+ visitsPerDay.push(visits);
+ }
+ }
+ return visitsPerDay;
+ }
+
+ /**
+ * Get a list of visits per month for each month, excluding this one, and
+ * excluding yesterday's visits if yesterday happens to fall on the previous
+ * month.
+ *
+ * @param {Map<number, HistoryVisit[]>} cachedHistory
+ * The history cache to process.
+ * @returns {HistoryVisit[][]}
+ * A list of visits for each month.
+ */
+ #getVisitsByMonth(cachedHistory) {
+ const visitsPerMonth = [];
+ let previousMonth = null;
+ for (const [time, visits] of cachedHistory.entries()) {
+ const date = new Date(time);
+ if (
+ this.#isSameMonth(date, this.#todaysDate) ||
+ this.#isSameDate(date, this.#yesterdaysDate)
+ ) {
+ continue;
+ }
+ const month = this.placesQuery.getStartOfMonthTimestamp(date);
+ if (month !== previousMonth) {
+ visitsPerMonth.push(visits);
+ } else {
+ visitsPerMonth[visitsPerMonth.length - 1] = visitsPerMonth
+ .at(-1)
+ .concat(visits);
+ }
+ previousMonth = month;
+ }
+ return visitsPerMonth;
+ }
+
+ /**
+ * Given two date instances, check if their dates are equivalent.
+ *
+ * @param {Date} dateToCheck
+ * @param {Date} date
+ * @returns {boolean}
+ * Whether both date instances have equivalent dates.
+ */
+ #isSameDate(dateToCheck, date) {
+ return (
+ dateToCheck.getDate() === date.getDate() &&
+ this.#isSameMonth(dateToCheck, date)
+ );
+ }
+
+ /**
+ * Given two date instances, check if their months are equivalent.
+ *
+ * @param {Date} dateToCheck
+ * @param {Date} month
+ * @returns {boolean}
+ * Whether both date instances have equivalent months.
+ */
+ #isSameMonth(dateToCheck, month) {
+ return (
+ dateToCheck.getMonth() === month.getMonth() &&
+ dateToCheck.getFullYear() === month.getFullYear()
+ );
+ }
+
+ /**
+ * Get a list of visits, sorted by site, in alphabetical order.
+ *
+ * @param {Map<string, HistoryVisit[]>} historyMap
+ * @returns {CardEntry[]}
+ */
+ #getVisitsForSite(historyMap) {
+ return Array.from(historyMap.entries(), ([domain, items]) => ({
+ domain,
+ items,
+ l10nId: domain ? null : "firefoxview-history-site-localhost",
+ })).sort((a, b) => a.domain.localeCompare(b.domain));
+ }
+
+ async #fetchHistory() {
+ return this.placesQuery.getHistory({
+ daysOld: 60,
+ limit: lazy.maxRowsPref,
+ sortBy: this.sortOption,
+ });
+ }
+}
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..9462766545
--- /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/search-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..84c6acc5c4 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;
@@ -132,76 +132,71 @@ class CardContainer extends MozLitElement {
rel="stylesheet"
href="chrome://browser/content/firefoxview/card-container.css"
/>
- <section
- aria-labelledby="header"
- aria-label=${ifDefined(this.sectionLabel)}
- >
- ${when(
- this.toggleDisabled,
- () => html`<div
- class=${classMap({
- "card-container": true,
- inner: this.isInnerCard,
- "empty-state": this.isEmptyState && !this.isInnerCard,
- })}
+ ${when(
+ this.toggleDisabled,
+ () => html`<div
+ class=${classMap({
+ "card-container": true,
+ inner: this.isInnerCard,
+ "empty-state": this.isEmptyState && !this.isInnerCard,
+ })}
+ >
+ <span
+ id="header"
+ class="card-container-header"
+ ?hidden=${ifDefined(this.hideHeader)}
+ toggleDisabled
+ ?withViewAll=${this.showViewAll}
>
- <span
- id="header"
- class="card-container-header"
- ?hidden=${ifDefined(this.hideHeader)}
- toggleDisabled
- ?withViewAll=${this.showViewAll}
- >
- <slot name="header"></slot>
- <slot name="secondary-header"></slot>
- </span>
- <a
- href="about:firefoxview#${this.shortPageName}"
- @click=${this.viewAllClicked}
- class="view-all-link"
- data-l10n-id="firefoxview-view-all-link"
- ?hidden=${!this.showViewAll}
- ></a>
- <slot name="main"></slot>
- <slot name="footer" class="card-container-footer"></slot>
- </div>`,
- () => html`<details
- class=${classMap({
- "card-container": true,
- inner: this.isInnerCard,
- "empty-state": this.isEmptyState && !this.isInnerCard,
- })}
- ?open=${this.isExpanded}
- ?isOpenTabsView=${this.removeBlockEndMargin}
- @toggle=${this.onToggleContainer}
+ <slot name="header"></slot>
+ <slot name="secondary-header"></slot>
+ </span>
+ <a
+ href="about:firefoxview#${this.shortPageName}"
+ @click=${this.viewAllClicked}
+ class="view-all-link"
+ data-l10n-id="firefoxview-view-all-link"
+ ?hidden=${!this.showViewAll}
+ ></a>
+ <slot name="main"></slot>
+ <slot name="footer" class="card-container-footer"></slot>
+ </div>`,
+ () => html`<details
+ class=${classMap({
+ "card-container": true,
+ inner: this.isInnerCard,
+ "empty-state": this.isEmptyState && !this.isInnerCard,
+ })}
+ ?open=${this.isExpanded}
+ ?isOpenTabsView=${this.removeBlockEndMargin}
+ @toggle=${this.onToggleContainer}
+ role=${this.isInnerCard ? "presentation" : "group"}
+ >
+ <summary
+ class="card-container-header"
+ ?hidden=${ifDefined(this.hideHeader)}
+ ?withViewAll=${this.showViewAll}
>
- <summary
- id="header"
- class="card-container-header"
- ?hidden=${ifDefined(this.hideHeader)}
- ?withViewAll=${this.showViewAll}
- >
- <span
- class="icon chevron-icon"
- aria-role="presentation"
- data-l10n-id="firefoxview-collapse-button-${this.isExpanded
- ? "hide"
- : "show"}"
- ></span>
- <slot name="header"></slot>
- </summary>
- <a
- href="about:firefoxview#${this.shortPageName}"
- @click=${this.viewAllClicked}
- class="view-all-link"
- data-l10n-id="firefoxview-view-all-link"
- ?hidden=${!this.showViewAll}
- ></a>
- <slot name="main"></slot>
- <slot name="footer" class="card-container-footer"></slot>
- </details>`
- )}
- </section>
+ <span
+ class="icon chevron-icon"
+ role="presentation"
+ data-l10n-id="firefoxview-collapse-button-${this.isExpanded
+ ? "hide"
+ : "show"}"
+ ></span>
+ <slot name="header"></slot>
+ </summary>
+ <a
+ href="about:firefoxview#${this.shortPageName}"
+ @click=${this.viewAllClicked}
+ class="view-all-link"
+ data-l10n-id="firefoxview-view-all-link"
+ ?hidden=${!this.showViewAll}
+ ></a>
+ <slot name="main"></slot>
+ <slot name="footer" class="card-container-footer"></slot>
+ </details>`
+ )}
`;
}
}
diff --git a/browser/components/firefoxview/firefox-view-notification-manager.sys.mjs b/browser/components/firefoxview/firefox-view-notification-manager.sys.mjs
deleted file mode 100644
index 3f9056a7cd..0000000000
--- a/browser/components/firefoxview/firefox-view-notification-manager.sys.mjs
+++ /dev/null
@@ -1,112 +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 module exports the FirefoxViewNotificationManager singleton, which manages the notification state
- * for the Firefox View button
- */
-
-const RECENT_TABS_SYNC = "services.sync.lastTabFetch";
-const SHOULD_NOTIFY_FOR_TABS = "browser.tabs.firefox-view.notify-for-tabs";
-const lazy = {};
-
-import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
-
-ChromeUtils.defineESModuleGetters(lazy, {
- BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
- SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
-});
-
-export const FirefoxViewNotificationManager = new (class {
- #currentlyShowing;
- constructor() {
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- "lastTabFetch",
- RECENT_TABS_SYNC,
- 0,
- () => {
- this.handleTabSync();
- }
- );
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- "shouldNotifyForTabs",
- SHOULD_NOTIFY_FOR_TABS,
- false
- );
- // Need to access the pref variable for the observer to start observing
- // See the defineLazyPreferenceGetter function header
- this.lastTabFetch;
-
- Services.obs.addObserver(this, "firefoxview-notification-dot-update");
-
- this.#currentlyShowing = false;
- }
-
- async handleTabSync() {
- if (!this.shouldNotifyForTabs) {
- return;
- }
- let newSyncedTabs = await lazy.SyncedTabs.getRecentTabs(3);
- this.#currentlyShowing = this.tabsListChanged(newSyncedTabs);
- this.showNotificationDot();
- this.syncedTabs = newSyncedTabs;
- }
-
- showNotificationDot() {
- if (this.#currentlyShowing) {
- Services.obs.notifyObservers(
- null,
- "firefoxview-notification-dot-update",
- "true"
- );
- }
- }
-
- observe(sub, topic, data) {
- if (topic === "firefoxview-notification-dot-update" && data === "false") {
- this.#currentlyShowing = false;
- }
- }
-
- tabsListChanged(newTabs) {
- // The first time the tabs list is changed this.tabs is undefined because we haven't synced yet.
- // We don't want to show the badge here because it's not an actual change,
- // we are just syncing for the first time.
- if (!this.syncedTabs) {
- return false;
- }
-
- // We loop through all windows to see if any window has currentURI "about:firefoxview" and
- // the window is visible because we don't want to show the notification badge in that case
- for (let window of lazy.BrowserWindowTracker.orderedWindows) {
- // if the url is "about:firefoxview" and the window visible we don't want to show the notification badge
- if (
- window.FirefoxViewHandler.tab?.selected &&
- !window.isFullyOccluded &&
- window.windowState !== window.STATE_MINIMIZED
- ) {
- return false;
- }
- }
-
- if (newTabs.length > this.syncedTabs.length) {
- return true;
- }
- for (let i = 0; i < newTabs.length; i++) {
- let newTab = newTabs[i];
- let oldTab = this.syncedTabs[i];
-
- if (newTab?.url !== oldTab?.url) {
- return true;
- }
- }
- return false;
- }
-
- shouldNotificationDotBeShowing() {
- return this.#currentlyShowing;
- }
-})();
diff --git a/browser/components/firefoxview/firefox-view-places-query.sys.mjs b/browser/components/firefoxview/firefox-view-places-query.sys.mjs
deleted file mode 100644
index 8923905769..0000000000
--- a/browser/components/firefoxview/firefox-view-places-query.sys.mjs
+++ /dev/null
@@ -1,187 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-import { PlacesQuery } from "resource://gre/modules/PlacesQuery.sys.mjs";
-
-/**
- * Extension of PlacesQuery which provides additional caches for Firefox View.
- */
-export class FirefoxViewPlacesQuery extends PlacesQuery {
- /** @type {Date} */
- #todaysDate = null;
- /** @type {Date} */
- #yesterdaysDate = null;
-
- get visitsFromToday() {
- if (this.cachedHistory == null || this.#todaysDate == null) {
- return [];
- }
- const mapKey = this.getStartOfDayTimestamp(this.#todaysDate);
- return this.cachedHistory.get(mapKey) ?? [];
- }
-
- get visitsFromYesterday() {
- if (this.cachedHistory == null || this.#yesterdaysDate == null) {
- return [];
- }
- const mapKey = this.getStartOfDayTimestamp(this.#yesterdaysDate);
- return this.cachedHistory.get(mapKey) ?? [];
- }
-
- /**
- * Get a list of visits per day for each day on this month, excluding today
- * and yesterday.
- *
- * @returns {HistoryVisit[][]}
- * A list of visits for each day.
- */
- get visitsByDay() {
- const visitsPerDay = [];
- for (const [time, visits] of this.cachedHistory.entries()) {
- const date = new Date(time);
- if (
- this.#isSameDate(date, this.#todaysDate) ||
- this.#isSameDate(date, this.#yesterdaysDate)
- ) {
- continue;
- } else if (!this.#isSameMonth(date, this.#todaysDate)) {
- break;
- } else {
- visitsPerDay.push(visits);
- }
- }
- return visitsPerDay;
- }
-
- /**
- * Get a list of visits per month for each month, excluding this one, and
- * excluding yesterday's visits if yesterday happens to fall on the previous
- * month.
- *
- * @returns {HistoryVisit[][]}
- * A list of visits for each month.
- */
- get visitsByMonth() {
- const visitsPerMonth = [];
- let previousMonth = null;
- for (const [time, visits] of this.cachedHistory.entries()) {
- const date = new Date(time);
- if (
- this.#isSameMonth(date, this.#todaysDate) ||
- this.#isSameDate(date, this.#yesterdaysDate)
- ) {
- continue;
- }
- const month = this.getStartOfMonthTimestamp(date);
- if (month !== previousMonth) {
- visitsPerMonth.push(visits);
- } else {
- visitsPerMonth[visitsPerMonth.length - 1] = visitsPerMonth
- .at(-1)
- .concat(visits);
- }
- previousMonth = month;
- }
- return visitsPerMonth;
- }
-
- formatRowAsVisit(row) {
- const visit = super.formatRowAsVisit(row);
- this.#normalizeVisit(visit);
- return visit;
- }
-
- formatEventAsVisit(event) {
- const visit = super.formatEventAsVisit(event);
- this.#normalizeVisit(visit);
- return visit;
- }
-
- /**
- * Normalize data for fxview-tabs-list.
- *
- * @param {HistoryVisit} visit
- * The visit to format.
- */
- #normalizeVisit(visit) {
- visit.time = visit.date.getTime();
- visit.title = visit.title || visit.url;
- visit.icon = `page-icon:${visit.url}`;
- visit.primaryL10nId = "fxviewtabrow-tabs-list-tab";
- visit.primaryL10nArgs = JSON.stringify({
- targetURI: visit.url,
- });
- visit.secondaryL10nId = "fxviewtabrow-options-menu-button";
- visit.secondaryL10nArgs = JSON.stringify({
- tabTitle: visit.title || visit.url,
- });
- }
-
- async fetchHistory() {
- await super.fetchHistory();
- if (this.cachedHistoryOptions.sortBy === "date") {
- this.#setTodaysDate();
- }
- }
-
- handlePageVisited(event) {
- const visit = super.handlePageVisited(event);
- if (!visit) {
- return;
- }
- if (
- this.cachedHistoryOptions.sortBy === "date" &&
- (this.#todaysDate == null ||
- (visit.date.getTime() > this.#todaysDate.getTime() &&
- !this.#isSameDate(visit.date, this.#todaysDate)))
- ) {
- // If today's date has passed (or is null), it should be updated now.
- this.#setTodaysDate();
- }
- }
-
- #setTodaysDate() {
- const now = new Date();
- this.#todaysDate = new Date(
- now.getFullYear(),
- now.getMonth(),
- now.getDate()
- );
- this.#yesterdaysDate = new Date(
- now.getFullYear(),
- now.getMonth(),
- now.getDate() - 1
- );
- }
-
- /**
- * Given two date instances, check if their dates are equivalent.
- *
- * @param {Date} dateToCheck
- * @param {Date} date
- * @returns {boolean}
- * Whether both date instances have equivalent dates.
- */
- #isSameDate(dateToCheck, date) {
- return (
- dateToCheck.getDate() === date.getDate() &&
- this.#isSameMonth(dateToCheck, date)
- );
- }
-
- /**
- * Given two date instances, check if their months are equivalent.
- *
- * @param {Date} dateToCheck
- * @param {Date} month
- * @returns {boolean}
- * Whether both date instances have equivalent months.
- */
- #isSameMonth(dateToCheck, month) {
- return (
- dateToCheck.getMonth() === month.getMonth() &&
- dateToCheck.getFullYear() === month.getFullYear()
- );
- }
-}
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..0789c887bf 100644
--- a/browser/components/firefoxview/firefoxview.css
+++ b/browser/components/firefoxview/firefoxview.css
@@ -47,7 +47,7 @@
}
}
-@media (prefers-contrast) {
+@media (forced-colors) {
:root {
--fxview-element-background-hover: ButtonText;
--fxview-element-background-active: ButtonText;
@@ -59,6 +59,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..bdaa41bd7c 100644
--- a/browser/components/firefoxview/firefoxview.html
+++ b/browser/components/firefoxview/firefoxview.html
@@ -13,7 +13,6 @@
<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="toolkit/branding/brandings.ftl" />
<link rel="localization" href="browser/migrationWizard.ftl" />
@@ -72,6 +71,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 +95,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 +107,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-empty-state.mjs b/browser/components/firefoxview/fxview-empty-state.mjs
index 9e6bc488fa..3e94767043 100644
--- a/browser/components/firefoxview/fxview-empty-state.mjs
+++ b/browser/components/firefoxview/fxview-empty-state.mjs
@@ -53,7 +53,6 @@ class FxviewEmptyState extends MozLitElement {
return html``;
}
return html` <a
- aria-details="card-container"
data-l10n-name=${descriptionLink.name}
href=${descriptionLink.url}
target=${descriptionLink?.sameTarget ? "_self" : "_blank"}
@@ -68,7 +67,7 @@ class FxviewEmptyState extends MozLitElement {
/>
<card-container hideHeader="true" exportparts="image" ?isInnerCard="${
this.isInnerCard
- }" id="card-container" isEmptyState="true">
+ }" id="card-container" isEmptyState="true" role="group" aria-labelledby="header" aria-describedby="description">
<div slot="main" class=${classMap({
selectedTab: this.isSelectedTab,
imageHidden: !this.mainImageUrl,
@@ -98,19 +97,21 @@ class FxviewEmptyState extends MozLitElement {
data-l10n-args="${JSON.stringify(this.headerArgs)}">
</span>
</h2>
- ${repeat(
- this.descriptionLabels,
- descLabel => descLabel,
- (descLabel, index) => html`<p
- class=${classMap({
- description: true,
- secondary: index !== 0,
- })}
- data-l10n-id="${descLabel}"
- >
- ${this.linkTemplate(this.descriptionLink)}
- </p>`
- )}
+ <span id="description">
+ ${repeat(
+ this.descriptionLabels,
+ descLabel => descLabel,
+ (descLabel, index) => html`<p
+ class=${classMap({
+ description: true,
+ secondary: index !== 0,
+ })}
+ data-l10n-id="${descLabel}"
+ >
+ ${this.linkTemplate(this.descriptionLink)}
+ </p>`
+ )}
+ </span>
<slot name="primary-action"></slot>
</div>
</div>
diff --git a/browser/components/firefoxview/fxview-search-textbox.mjs b/browser/components/firefoxview/fxview-search-textbox.mjs
index 1332f5f3f6..107aa8f7a4 100644
--- a/browser/components/firefoxview/fxview-search-textbox.mjs
+++ b/browser/components/firefoxview/fxview-search-textbox.mjs
@@ -20,7 +20,7 @@ const SEARCH_DEBOUNCE_TIMEOUT_MS = 1000;
*
* There is no actual searching done here. That needs to be implemented by the
* `fxview-search-textbox-query` event handler. `searchTabList()` from
- * `helpers.mjs` can be used as a starting point.
+ * `search-helpers.mjs` can be used as a starting point.
*
* @property {string} placeholder
* The placeholder text for the search box.
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..63be9379db 100644
--- a/browser/components/firefoxview/fxview-tab-list.mjs
+++ b/browser/components/firefoxview/fxview-tab-list.mjs
@@ -11,7 +11,9 @@ import {
when,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
-import { escapeRegExp } from "./helpers.mjs";
+import { escapeRegExp } from "./search-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,12 @@ 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} 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,9 +63,6 @@ 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.updatesPaused = true;
this.#register();
@@ -77,7 +75,6 @@ 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 },
@@ -86,7 +83,10 @@ export default class FxviewTabList extends MozLitElement {
};
static queries = {
- rowEls: { all: "fxview-tab-row" },
+ emptyState: "fxview-empty-state",
+ rowEls: {
+ all: "fxview-tab-row",
+ },
rootVirtualListEl: "virtual-list",
};
@@ -108,20 +108,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 +135,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 +184,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 +220,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 +267,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 +285,32 @@ 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) {
+ 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 +323,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 +347,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 +376,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 +403,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 +423,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 +531,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 +552,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 +584,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 +669,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>`;
+ }
+
+ 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>`
+ )}`;
+ }
+}
- return html`<a
+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 +757,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 +804,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 +828,8 @@ export class VirtualList extends MozLitElement {
disconnectedCallback() {
super.disconnectedCallback();
this.intersectionObserver.disconnect();
- this.resizeObserver.disconnect();
+ this.childResizeObserver.disconnect();
+ this.selfResizeObserver.disconnect();
}
triggerIntersectionObserver() {
@@ -1090,7 +861,6 @@ export class VirtualList extends MozLitElement {
this.items.slice(i, i + this.maxRenderCountEstimate)
);
}
- this.triggerIntersectionObserver();
}
}
@@ -1103,13 +873,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 +931,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..fb41fac0e1 100644
--- a/browser/components/firefoxview/helpers.mjs
+++ b/browser/components/firefoxview/helpers.mjs
@@ -126,27 +126,6 @@ export function isSearchEnabled() {
}
/**
- * Escape special characters for regular expressions from a string.
- *
- * @param {string} string
- * The string to sanitize.
- * @returns {string} The sanitized string.
- */
-export function escapeRegExp(string) {
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-}
-
-/**
- * Search a tab list for items that match the given query.
- */
-export function searchTabList(query, tabList) {
- const regex = RegExp(escapeRegExp(query), "i");
- return tabList.filter(
- ({ title, url }) => regex.test(title) || regex.test(url)
- );
-}
-
-/**
* Get or create a logger, whose log-level is controlled by a pref
*
* @param {string} loggerName - Creating named loggers helps differentiate log messages from different
@@ -173,3 +152,20 @@ export function escapeHtmlEntities(text) {
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
+
+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..4919f94e9c 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";
+// 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",
+ HistoryController: "resource:///modules/HistoryController.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 lazy.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.updateCache();
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;
+ onChangeSortOption(e) {
+ 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();
+ }
+
+ onSearchQuery(e) {
+ 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.isHistoryEmpty) {
return this.#historyCardsTemplate();
}
return this.#emptyMessageTemplate();
@@ -406,11 +295,11 @@ class HistoryInView extends ViewPage {
#historyCardsTemplate() {
let cardsTemplate = [];
- if (this.sortOption === "date" && this.historyMapByDate.length) {
- this.historyMapByDate.forEach(historyItem => {
- if (historyItem.items.length) {
+ switch (this.controller.sortOption) {
+ case "date":
+ cardsTemplate = this.controller.historyVisits.map(historyItem => {
let dateArg = JSON.stringify({ date: historyItem.items[0].time });
- cardsTemplate.push(html`<card-container>
+ return html`<card-container>
<h3
slot="header"
data-l10n-id=${historyItem.l10nId}
@@ -430,13 +319,12 @@ class HistoryInView extends ViewPage {
>
${this.panelListTemplate()}
</fxview-tab-list>
- </card-container>`);
- }
- });
- } else if (this.historyMapBySite.length) {
- this.historyMapBySite.forEach(historyItem => {
- if (historyItem.items.length) {
- cardsTemplate.push(html`<card-container>
+ </card-container>`;
+ });
+ break;
+ case "site":
+ cardsTemplate = this.controller.historyVisits.map(historyItem => {
+ return html`<card-container>
<h3 slot="header" data-l10n-id="${ifDefined(historyItem.l10nId)}">
${historyItem.domain}
</h3>
@@ -452,9 +340,9 @@ class HistoryInView extends ViewPage {
>
${this.panelListTemplate()}
</fxview-tab-list>
- </card-container>`);
- }
- });
+ </card-container>`;
+ });
+ break;
}
return cardsTemplate;
}
@@ -504,17 +392,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,8 +412,8 @@ 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}
>
@@ -569,7 +457,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 +471,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
@@ -598,12 +486,19 @@ class HistoryInView extends ViewPage {
class="import-history-banner"
hideHeader="true"
?hidden=${!this.shouldShowImportBanner()}
+ role="group"
+ aria-labelledby="header"
+ aria-describedby="description"
>
<div slot="main">
<div class="banner-text">
- <span data-l10n-id="firefoxview-import-history-header"></span>
+ <span
+ data-l10n-id="firefoxview-import-history-header"
+ id="header"
+ ></span>
<span
data-l10n-id="firefoxview-import-history-description"
+ id="description"
></span>
</div>
<div class="buttons">
@@ -612,11 +507,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,33 +520,20 @@ class HistoryInView extends ViewPage {
</div>
<div
class="show-all-history-footer"
- ?hidden=${!this.allHistoryItems.size}
+ ?hidden=${this.controller.isHistoryEmpty}
>
<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")) {
- // onChangeSortOption() will update history data once it has been fetched
- // from the API.
- this.createHistoryMaps();
- }
}
}
customElements.define("view-history", HistoryInView);
diff --git a/browser/components/firefoxview/jar.mn b/browser/components/firefoxview/jar.mn
index 1e5cc3e690..6eee0b8ffd 100644
--- a/browser/components/firefoxview/jar.mn
+++ b/browser/components/firefoxview/jar.mn
@@ -18,11 +18,15 @@ browser.jar:
content/browser/firefoxview/fxview-empty-state.css
content/browser/firefoxview/fxview-empty-state.mjs
content/browser/firefoxview/helpers.mjs
+ content/browser/firefoxview/search-helpers.mjs
content/browser/firefoxview/fxview-search-textbox.css
content/browser/firefoxview/fxview-search-textbox.mjs
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..10845374bc 100644
--- a/browser/components/firefoxview/opentabs.mjs
+++ b/browser/components/firefoxview/opentabs.mjs
@@ -13,10 +13,12 @@ import {
getLogger,
isSearchEnabled,
placeLinkOnClipboard,
- searchTabList,
MAX_TABS_FOR_RECENT_BROWSING,
} from "./helpers.mjs";
+import { searchTabList } from "./search-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..6b3ed711c4 100644
--- a/browser/components/firefoxview/recentlyclosed.mjs
+++ b/browser/components/firefoxview/recentlyclosed.mjs
@@ -8,11 +8,8 @@ import {
ifDefined,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
-import {
- isSearchEnabled,
- searchTabList,
- MAX_TABS_FOR_RECENT_BROWSING,
-} from "./helpers.mjs";
+import { isSearchEnabled, MAX_TABS_FOR_RECENT_BROWSING } from "./helpers.mjs";
+import { searchTabList } from "./search-helpers.mjs";
import { ViewPage } from "./viewpage.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/firefoxview/card-container.mjs";
@@ -65,7 +62,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 +246,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 +393,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/search-helpers.mjs b/browser/components/firefoxview/search-helpers.mjs
new file mode 100644
index 0000000000..3a8c1e580c
--- /dev/null
+++ b/browser/components/firefoxview/search-helpers.mjs
@@ -0,0 +1,24 @@
+/* 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/. */
+
+/**
+ * Escape special characters for regular expressions from a string.
+ *
+ * @param {string} string
+ * The string to sanitize.
+ * @returns {string} The sanitized string.
+ */
+export function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+/**
+ * Search a tab list for items that match the given query.
+ */
+export function searchTabList(query, tabList) {
+ const regex = RegExp(escapeRegExp(query), "i");
+ return tabList.filter(
+ ({ title, url }) => regex.test(title) || regex.test(url)
+ );
+}
diff --git a/browser/components/firefoxview/syncedtabs.mjs b/browser/components/firefoxview/syncedtabs.mjs
index d64da45a30..e71cce465e 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,36 +169,26 @@ class SyncedTabsInView extends ViewPage {
?hidden=${!buttonLabel}
data-l10n-id="${ifDefined(buttonLabel)}"
data-action="${action}"
- @click=${this.handleEvent}
- aria-details="empty-container"
+ @click=${e => this.controller.handleEvent(e)}
></button>
</fxview-empty-state>
`;
}
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 +244,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 +265,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 +283,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 +295,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 +346,7 @@ class SyncedTabsInView extends ViewPage {
isShowAllLinkVisible(tabItems) {
return (
this.recentBrowsing &&
- this.searchQuery &&
+ this.controller.searchQuery &&
tabItems.length > this.maxTabsLength &&
!this.showAll
);
@@ -536,35 +373,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 +401,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 +418,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 +447,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 +468,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..c9036286d7 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"]
@@ -41,9 +43,6 @@ skip-if = ["true"] # Bug 1869605 and # Bug 1870296
["browser_history_firefoxview.js"]
-["browser_notification_dot.js"]
-skip-if = ["true"] # Bug 1851453
-
["browser_opentabs_cards.js"]
["browser_opentabs_changes.js"]
@@ -51,17 +50,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.js b/browser/components/firefoxview/tests/browser/browser_firefoxview.js
index 1a51d61f42..00083d7c91 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview.js
@@ -31,20 +31,47 @@ add_task(async function test_aria_roles() {
);
let recentlyClosedEmptyState = recentlyClosedComponent.emptyState;
let descriptionEls = recentlyClosedEmptyState.descriptionEls;
+ const recentlyClosedCard = SpecialPowers.wrap(
+ recentlyClosedEmptyState
+ ).openOrClosedShadowRoot.querySelector("card-container");
is(
- descriptionEls[1].querySelector("a").getAttribute("aria-details"),
- "card-container",
- "The link within the recently closed empty state has the expected 'aria-details' attribute."
+ recentlyClosedCard.getAttribute("aria-labelledby"),
+ "header",
+ "The recently closed empty state container has the expected 'aria-labelledby' attribute."
+ );
+ is(
+ recentlyClosedCard.getAttribute("aria-describedby"),
+ "description",
+ "The recently closed empty state container has the expected 'aria-describedby' attribute."
+ );
+ is(
+ recentlyClosedCard.getAttribute("role"),
+ "group",
+ "The recently closed empty state container has the expected 'role' attribute."
);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs[slot=syncedtabs]"
);
let syncedTabsEmptyState = syncedTabsComponent.emptyState;
+ const syncedCard =
+ SpecialPowers.wrap(
+ syncedTabsEmptyState
+ ).openOrClosedShadowRoot.querySelector("card-container");
+ is(
+ syncedCard.getAttribute("aria-labelledby"),
+ "header",
+ "The synced tabs empty state container has the expected 'aria-labelledby' attribute."
+ );
+ is(
+ syncedCard.getAttribute("aria-describedby"),
+ "description",
+ "The synced tabs empty state container has the expected 'aria-describedby' attribute."
+ );
is(
- syncedTabsEmptyState.querySelector("button").getAttribute("aria-details"),
- "empty-container",
- "The button within the synced tabs empty state has the expected 'aria-details' attribute."
+ syncedCard.getAttribute("role"),
+ "group",
+ "The synced tabs empty state container has the expected 'role' attribute."
);
// Test keyboard navigation from card-container summary
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_firefoxview_virtual_list.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js
index bf53796ef7..e9502079d9 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_virtual_list.js
@@ -32,7 +32,9 @@ add_task(async function test_max_render_count_on_win_resize() {
await navigateToViewAndWait(document, "history");
let historyComponent = document.querySelector("view-history");
- let tabList = historyComponent.lists[0];
+ let tabList = await TestUtils.waitForCondition(
+ () => historyComponent.lists[0]
+ );
let rootVirtualList = tabList.rootVirtualListEl;
const initialHeight = window.outerHeight;
diff --git a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
index c4c096acff..0bbc009eab 100644
--- a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
@@ -57,15 +57,11 @@ function isElInViewport(element) {
async function historyComponentReady(historyComponent, expectedHistoryItems) {
await TestUtils.waitForCondition(
- () =>
- [...historyComponent.allHistoryItems.values()].reduce(
- (acc, { length }) => acc + length,
- 0
- ) === expectedHistoryItems,
+ () => historyComponent.controller.totalVisitsCount === expectedHistoryItems,
"History component ready"
);
- let expected = historyComponent.historyMapByDate.length;
+ let expected = historyComponent.controller.historyVisits.length;
let actual = historyComponent.cards.length;
is(expected, actual, `Total number of cards should be ${expected}`);
@@ -242,7 +238,7 @@ add_task(async function test_list_ordering() {
await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
await sortHistoryTelemetry(sortHistoryEvent);
- let expectedNumOfCards = historyComponent.historyMapBySite.length;
+ let expectedNumOfCards = historyComponent.controller.historyVisits.length;
info(`Total number of cards should be ${expectedNumOfCards}`);
await BrowserTestUtils.waitForMutationCondition(
@@ -345,7 +341,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(
@@ -474,7 +470,7 @@ add_task(async function test_search_history() {
EventUtils.sendString("Bogus Query", content);
await TestUtils.waitForCondition(() => {
const tabList = historyComponent.lists[0];
- return tabList?.shadowRoot.querySelector("fxview-empty-state");
+ return tabList?.emptyState;
}, "There are no matching search results.");
info("Clear the search query.");
@@ -484,7 +480,7 @@ add_task(async function test_search_history() {
{ childList: true, subtree: true },
() =>
historyComponent.cards.length ===
- historyComponent.historyMapByDate.length
+ historyComponent.controller.historyVisits.length
);
searchTextbox.blur();
@@ -493,7 +489,7 @@ add_task(async function test_search_history() {
EventUtils.sendString("Bogus Query", content);
await TestUtils.waitForCondition(() => {
const tabList = historyComponent.lists[0];
- return tabList?.shadowRoot.querySelector("fxview-empty-state");
+ return tabList?.emptyState;
}, "There are no matching search results.");
info("Clear the search query with keyboard.");
@@ -513,11 +509,69 @@ add_task(async function test_search_history() {
{ childList: true, subtree: true },
() =>
historyComponent.cards.length ===
- historyComponent.historyMapByDate.length
+ historyComponent.controller.historyVisits.length
);
});
});
+add_task(async function test_search_ignores_stale_queries() {
+ await PlacesUtils.history.clear();
+ const historyEntries = createHistoryEntries();
+ await PlacesUtils.history.insertMany(historyEntries);
+
+ let bogusQueryInProgress = false;
+ const searchDeferred = Promise.withResolvers();
+ const realDatabase = await PlacesUtils.promiseLargeCacheDBConnection();
+ const mockDatabase = {
+ executeCached: async (sql, options) => {
+ if (options.query === "Bogus Query") {
+ bogusQueryInProgress = true;
+ await searchDeferred.promise;
+ }
+ return realDatabase.executeCached(sql, options);
+ },
+ interrupt: () => searchDeferred.reject(),
+ };
+ const stub = sinon
+ .stub(PlacesUtils, "promiseLargeCacheDBConnection")
+ .resolves(mockDatabase);
+
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "history");
+ const historyComponent = document.querySelector("view-history");
+ historyComponent.profileAge = 8;
+ await historyComponentReady(historyComponent, historyEntries.length);
+ const searchTextbox = await TestUtils.waitForCondition(
+ () => historyComponent.searchTextbox,
+ "The search textbox is displayed."
+ );
+
+ info("Input a bogus search query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
+ EventUtils.sendString("Bogus Query", content);
+ await TestUtils.waitForCondition(() => bogusQueryInProgress);
+
+ info("Clear the bogus query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox.clearButton, {}, content);
+ await searchTextbox.updateComplete;
+
+ info("Input a real search query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, content);
+ EventUtils.sendString("Example Domain 1", content);
+ await TestUtils.waitForCondition(() => {
+ const { rowEls } = historyComponent.lists[0];
+ return rowEls.length === 1 && rowEls[0].mainEl.href === URLs[1];
+ }, "There is one matching search result.");
+ searchDeferred.resolve();
+ await TestUtils.waitForTick();
+ const tabList = historyComponent.lists[0];
+ ok(!tabList.emptyState, "Empty state should not be shown.");
+ });
+
+ stub.restore();
+});
+
add_task(async function test_persist_collapse_card_after_view_change() {
await PlacesUtils.history.clear();
await addHistoryItems(today);
@@ -527,11 +581,7 @@ add_task(async function test_persist_collapse_card_after_view_change() {
const historyComponent = document.querySelector("view-history");
historyComponent.profileAge = 8;
await TestUtils.waitForCondition(
- () =>
- [...historyComponent.allHistoryItems.values()].reduce(
- (acc, { length }) => acc + length,
- 0
- ) === 4
+ () => historyComponent.controller.totalVisitsCount === 4
);
let firstHistoryCard = historyComponent.cards[0];
ok(
diff --git a/browser/components/firefoxview/tests/browser/browser_notification_dot.js b/browser/components/firefoxview/tests/browser/browser_notification_dot.js
deleted file mode 100644
index 0fa747d40f..0000000000
--- a/browser/components/firefoxview/tests/browser/browser_notification_dot.js
+++ /dev/null
@@ -1,392 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const tabsList1 = syncedTabsData1[0].tabs;
-const tabsList2 = syncedTabsData1[1].tabs;
-const BADGE_TOP_RIGHT = "75% 25%";
-
-const { SyncedTabs } = ChromeUtils.importESModule(
- "resource://services-sync/SyncedTabs.sys.mjs"
-);
-
-function setupRecentDeviceListMocks() {
- const sandbox = sinon.createSandbox();
- sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => [
- {
- id: 1,
- name: "My desktop",
- isCurrentDevice: true,
- type: "desktop",
- tabs: [],
- },
- {
- id: 2,
- name: "My iphone",
- type: "mobile",
- tabs: [],
- },
- ]);
-
- sandbox.stub(UIState, "get").returns({
- status: UIState.STATUS_SIGNED_IN,
- syncEnabled: true,
- });
-
- return sandbox;
-}
-
-function waitForWindowActive(win, active) {
- info("Waiting for window activation");
- return Promise.all([
- BrowserTestUtils.waitForEvent(win, active ? "focus" : "blur"),
- BrowserTestUtils.waitForEvent(win, active ? "activate" : "deactivate"),
- ]);
-}
-
-async function waitForNotificationBadgeToBeShowing(fxViewButton) {
- info("Waiting for attention attribute to be set");
- await BrowserTestUtils.waitForMutationCondition(
- fxViewButton,
- { attributes: true },
- () => fxViewButton.hasAttribute("attention")
- );
- return fxViewButton.hasAttribute("attention");
-}
-
-async function waitForNotificationBadgeToBeHidden(fxViewButton) {
- info("Waiting for attention attribute to be removed");
- await BrowserTestUtils.waitForMutationCondition(
- fxViewButton,
- { attributes: true },
- () => !fxViewButton.hasAttribute("attention")
- );
- return !fxViewButton.hasAttribute("attention");
-}
-
-async function clickFirefoxViewButton(win) {
- await BrowserTestUtils.synthesizeMouseAtCenter(
- "#firefox-view-button",
- { type: "mousedown" },
- win.browsingContext
- );
-}
-
-function getBackgroundPositionForElement(ele) {
- let style = ele.ownerGlobal.getComputedStyle(ele);
- return style.getPropertyValue("background-position");
-}
-
-let previousFetchTime = 0;
-
-async function resetSyncedTabsLastFetched() {
- Services.prefs.clearUserPref("services.sync.lastTabFetch");
- previousFetchTime = 0;
- await TestUtils.waitForTick();
-}
-
-async function initTabSync() {
- let recentFetchTime = Math.floor(Date.now() / 1000);
- // ensure we don't try to set the pref with the same value, which will not produce
- // the expected pref change effects
- while (recentFetchTime == previousFetchTime) {
- await TestUtils.waitForTick();
- recentFetchTime = Math.floor(Date.now() / 1000);
- }
- Assert.greater(
- recentFetchTime,
- previousFetchTime,
- "The new lastTabFetch value is greater than the previous"
- );
-
- info("initTabSync, updating lastFetch:" + recentFetchTime);
- Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
- previousFetchTime = recentFetchTime;
- await TestUtils.waitForTick();
-}
-
-add_setup(async function () {
- await resetSyncedTabsLastFetched();
- await SpecialPowers.pushPrefEnv({
- set: [["browser.tabs.firefox-view.notify-for-tabs", true]],
- });
-
- // Clear any synced tabs from previous tests
- FirefoxViewNotificationManager.syncedTabs = null;
- Services.obs.notifyObservers(
- null,
- "firefoxview-notification-dot-update",
- "false"
- );
-});
-
-/**
- * Test that the notification badge will show and hide in the correct cases
- */
-add_task(async function testNotificationDot() {
- const sandbox = setupRecentDeviceListMocks();
- const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
- sandbox.spy(SyncedTabs, "syncTabs");
-
- let win = await BrowserTestUtils.openNewBrowserWindow();
- let fxViewBtn = win.document.getElementById("firefox-view-button");
- ok(fxViewBtn, "Got the Firefox View button");
-
- // Initiate a synced tabs update with new tabs
- syncedTabsMock.returns(tabsList1);
- await initTabSync();
-
- ok(
- BrowserTestUtils.isVisible(fxViewBtn),
- "The Firefox View button is showing"
- );
-
- info(
- "testNotificationDot, button is showing, badge should be initially hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing initially"
- );
-
- // Initiate a synced tabs update with new tabs
- syncedTabsMock.returns(tabsList2);
- await initTabSync();
-
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing after first tab sync"
- );
-
- // check that switching to the firefoxviewtab removes the badge
- await clickFirefoxViewButton(win);
-
- info(
- "testNotificationDot, after clicking the button, badge should become hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing after going to Firefox View"
- );
-
- await BrowserTestUtils.waitForCondition(() => {
- return SyncedTabs.syncTabs.calledOnce;
- });
-
- ok(SyncedTabs.syncTabs.calledOnce, "SyncedTabs.syncTabs() was called once");
-
- syncedTabsMock.returns(tabsList1);
- // Initiate a synced tabs update with new tabs
- await initTabSync();
-
- // The noti badge would show but we are on a Firefox View page so no need to show the noti badge
- info(
- "testNotificationDot, after updating the recent tabs, badge should be hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing after tab sync while Firefox View is focused"
- );
-
- let newTab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
- syncedTabsMock.returns(tabsList2);
- await initTabSync();
-
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing after navigation to a new tab"
- );
-
- // check that switching back to the Firefox View tab removes the badge
- await clickFirefoxViewButton(win);
-
- info(
- "testNotificationDot, after switching back to fxview, badge should be hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing after focusing the Firefox View tab"
- );
-
- await BrowserTestUtils.switchTab(win.gBrowser, newTab);
-
- // Initiate a synced tabs update with no new tabs
- await initTabSync();
-
- info(
- "testNotificationDot, after switching back to fxview with no new tabs, badge should be hidden"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing after a tab sync with the same tabs"
- );
-
- await BrowserTestUtils.closeWindow(win);
-
- sandbox.restore();
-});
-
-/**
- * Tests the notification badge with multiple windows
- */
-add_task(async function testNotificationDotOnMultipleWindows() {
- const sandbox = setupRecentDeviceListMocks();
-
- await resetSyncedTabsLastFetched();
- const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
-
- // Create a new window
- let win1 = await BrowserTestUtils.openNewBrowserWindow();
- await win1.delayedStartupPromise;
- let fxViewBtn = win1.document.getElementById("firefox-view-button");
- ok(fxViewBtn, "Got the Firefox View button");
-
- syncedTabsMock.returns(tabsList1);
- // Initiate a synced tabs update
- await initTabSync();
-
- // Create another window
- let win2 = await BrowserTestUtils.openNewBrowserWindow();
- await win2.delayedStartupPromise;
- let fxViewBtn2 = win2.document.getElementById("firefox-view-button");
-
- await clickFirefoxViewButton(win2);
-
- // Make sure the badge doesn't show on any window
- info(
- "testNotificationDotOnMultipleWindows, badge is initially hidden on window 1"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn),
- "The notification badge is not showing in the inital window"
- );
- info(
- "testNotificationDotOnMultipleWindows, badge is initially hidden on window 2"
- );
- ok(
- await waitForNotificationBadgeToBeHidden(fxViewBtn2),
- "The notification badge is not showing in the second window"
- );
-
- // Minimize the window.
- win2.minimize();
-
- await TestUtils.waitForCondition(
- () => !win2.gBrowser.selectedBrowser.docShellIsActive,
- "Waiting for docshell to be marked as inactive after minimizing the window"
- );
-
- syncedTabsMock.returns(tabsList2);
- info("Initiate a synced tabs update with new tabs");
- await initTabSync();
-
- // The badge will show because the View tab is minimized
- // Make sure the badge shows on all windows
- info(
- "testNotificationDotOnMultipleWindows, after new synced tabs and minimized win2, badge is visible on window 1"
- );
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing in the initial window"
- );
- info(
- "testNotificationDotOnMultipleWindows, after new synced tabs and minimized win2, badge is visible on window 2"
- );
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn2),
- "The notification badge is showing in the second window"
- );
-
- win2.restore();
- await TestUtils.waitForCondition(
- () => win2.gBrowser.selectedBrowser.docShellIsActive,
- "Waiting for docshell to be marked as active after restoring the window"
- );
-
- await BrowserTestUtils.closeWindow(win1);
- await BrowserTestUtils.closeWindow(win2);
-
- sandbox.restore();
-});
-
-/**
- * Tests the notification badge is in the correct spot and that the badge shows when opening a new window
- * if another window is showing the badge
- */
-add_task(async function testNotificationDotLocation() {
- const sandbox = setupRecentDeviceListMocks();
- await resetSyncedTabsLastFetched();
- const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
-
- syncedTabsMock.returns(tabsList1);
-
- let win1 = await BrowserTestUtils.openNewBrowserWindow();
- let fxViewBtn = win1.document.getElementById("firefox-view-button");
- ok(fxViewBtn, "Got the Firefox View button");
-
- // Initiate a synced tabs update
- await initTabSync();
- syncedTabsMock.returns(tabsList2);
- // Initiate another synced tabs update
- await initTabSync();
-
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing initially"
- );
-
- // Create a new window
- let win2 = await BrowserTestUtils.openNewBrowserWindow();
- await win2.delayedStartupPromise;
-
- // Make sure the badge is showing on the new window
- let fxViewBtn2 = win2.document.getElementById("firefox-view-button");
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn2),
- "The notification badge is showing in the second window after opening"
- );
-
- // Make sure the badge is below and center now
- isnot(
- getBackgroundPositionForElement(fxViewBtn),
- BADGE_TOP_RIGHT,
- "The notification badge is not showing in the top right in the initial window"
- );
- isnot(
- getBackgroundPositionForElement(fxViewBtn2),
- BADGE_TOP_RIGHT,
- "The notification badge is not showing in the top right in the second window"
- );
-
- CustomizableUI.addWidgetToArea(
- "firefox-view-button",
- CustomizableUI.AREA_NAVBAR
- );
-
- // Make sure both windows still have the notification badge
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn),
- "The notification badge is showing in the initial window"
- );
- ok(
- await waitForNotificationBadgeToBeShowing(fxViewBtn2),
- "The notification badge is showing in the second window"
- );
-
- // Make sure the badge is in the top right now
- is(
- getBackgroundPositionForElement(fxViewBtn),
- BADGE_TOP_RIGHT,
- "The notification badge is showing in the top right in the initial window"
- );
- is(
- getBackgroundPositionForElement(fxViewBtn2),
- BADGE_TOP_RIGHT,
- "The notification badge is showing in the top right in the second window"
- );
-
- CustomizableUI.reset();
- await BrowserTestUtils.closeWindow(win1);
- await BrowserTestUtils.closeWindow(win2);
-
- sandbox.restore();
-});
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..872efd37a0 100644
--- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
@@ -13,6 +13,11 @@ add_setup(async function () {
registerCleanupFunction(async function () {
await tearDown(gSandbox);
});
+
+ // set tab sync false so we don't skip setup states
+ await SpecialPowers.pushPrefEnv({
+ set: [["services.sync.engine.tabs", false]],
+ });
});
async function promiseTabListsUpdated({ tabLists }) {
@@ -276,9 +281,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 +317,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");
@@ -745,3 +753,158 @@ add_task(async function search_synced_tabs_recent_browsing() {
await SpecialPowers.popPrefEnv();
await tearDown(sandbox);
});
+
+add_task(async function test_mobile_connected() {
+ Services.prefs.setBoolPref("services.sync.engine.tabs", false);
+ const sandbox = setupMocks({
+ state: UIState.STATUS_SIGNED_IN,
+ fxaDevices: [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ tabs: [],
+ },
+ {
+ id: 2,
+ name: "Other Device",
+ type: "mobile",
+ tabs: [],
+ },
+ ],
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ // ensure tab sync is false so we don't skip onto next step
+ ok(
+ !Services.prefs.getBoolPref("services.sync.engine.tabs", false),
+ "services.sync.engine.tabs is initially false"
+ );
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ await navigateToViewAndWait(document, "syncedtabs");
+
+ is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
+ ok(
+ fxAccounts.device.recentDeviceList?.some(
+ device => device.type == "mobile"
+ ),
+ "A connected device is type:mobile"
+ );
+ });
+ await tearDown(sandbox);
+ Services.prefs.setBoolPref("services.sync.engine.tabs", true);
+});
+
+add_task(async function test_tablet_connected() {
+ Services.prefs.setBoolPref("services.sync.engine.tabs", false);
+ const sandbox = setupMocks({
+ state: UIState.STATUS_SIGNED_IN,
+ fxaDevices: [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ tabs: [],
+ },
+ {
+ id: 2,
+ name: "Other Device",
+ type: "tablet",
+ tabs: [],
+ },
+ ],
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ // ensure tab sync is false so we don't skip onto next step
+ ok(
+ !Services.prefs.getBoolPref("services.sync.engine.tabs", false),
+ "services.sync.engine.tabs is initially false"
+ );
+
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ await navigateToViewAndWait(document, "syncedtabs");
+
+ is(fxAccounts.device.recentDeviceList?.length, 2, "2 devices connected");
+ ok(
+ fxAccounts.device.recentDeviceList?.some(
+ device => device.type == "tablet"
+ ),
+ "A connected device is type:tablet"
+ );
+ });
+ await tearDown(sandbox);
+ Services.prefs.setBoolPref("services.sync.engine.tabs", true);
+});
+
+add_task(async function test_tab_sync_enabled() {
+ const sandbox = setupMocks({
+ state: UIState.STATUS_SIGNED_IN,
+ fxaDevices: [
+ {
+ id: 1,
+ name: "This Device",
+ isCurrentDevice: true,
+ type: "desktop",
+ tabs: [],
+ },
+ {
+ id: 2,
+ name: "Other Device",
+ type: "mobile",
+ tabs: [],
+ },
+ ],
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+
+ // test initial state, with the pref not enabled
+ await navigateToViewAndWait(document, "syncedtabs");
+ // test with the pref toggled on
+ Services.prefs.setBoolPref("services.sync.engine.tabs", true);
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "Synced tabs component is fully updated."
+ );
+ ok(!syncedTabsComponent.emptyState, "No empty state is being displayed.");
+
+ // reset and test clicking the action button
+ Services.prefs.setBoolPref("services.sync.engine.tabs", false);
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "Synced tabs component is fully updated."
+ );
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.emptyState,
+ "The empty state is rendered."
+ );
+
+ const actionButton = syncedTabsComponent.emptyState?.querySelector(
+ "button[data-action=sync-tabs-disabled]"
+ );
+ EventUtils.synthesizeMouseAtCenter(actionButton, {}, browser.contentWindow);
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "Synced tabs component is fully updated."
+ );
+ await TestUtils.waitForCondition(
+ () => !syncedTabsComponent.emptyState,
+ "The empty state is rendered."
+ );
+
+ ok(true, "The empty state is no longer displayed when sync is enabled");
+ ok(
+ Services.prefs.getBoolPref("services.sync.engine.tabs", false),
+ "tab sync pref should be enabled after button click"
+ );
+ });
+ await tearDown(sandbox);
+});
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..abea8725ee 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">
@@ -42,8 +37,8 @@
const { BrowserTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/BrowserTestUtils.sys.mjs"
);
- const { FirefoxViewPlacesQuery } = ChromeUtils.importESModule(
- "resource:///modules/firefox-view-places-query.sys.mjs"
+ const { PlacesQuery } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesQuery.sys.mjs"
);
const { PlacesUtils } = ChromeUtils.importESModule(
"resource://gre/modules/PlacesUtils.sys.mjs"
@@ -57,7 +52,7 @@
const fxviewTabList = document.querySelector("fxview-tab-list");
let tabItems = [];
- const placesQuery = new FirefoxViewPlacesQuery();
+ const placesQuery = new PlacesQuery();
const URLs = [
"http://mochi.test:8888/browser/",
@@ -111,7 +106,20 @@
});
await historyUpdated.promise;
- fxviewTabList.tabItems = [...history.values()].flat();
+ fxviewTabList.tabItems = Array.from(history.values()).flat().map(visit => ({
+ ...visit,
+ time: visit.date.getTime(),
+ title: visit.title || visit.url,
+ icon: `page-icon:${visit.url}`,
+ primaryL10nId: "fxviewtabrow-tabs-list-tab",
+ primaryL10nArgs: JSON.stringify({
+ targetURI: visit.url,
+ }),
+ secondaryL10nId: "fxviewtabrow-options-menu-button",
+ secondaryL10nArgs: JSON.stringify({
+ tabTitle: visit.title || visit.url,
+ }),
+ }));
await fxviewTabList.getUpdateComplete();
tabItems = Array.from(fxviewTabList.rowEls);
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/limelight.svg b/browser/components/messagepreview/limelight.svg
index 938a17b3b2..2df3fffa5f 100644
--- a/browser/components/messagepreview/limelight.svg
+++ b/browser/components/messagepreview/limelight.svg
@@ -1,4 +1,4 @@
<!-- 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 version="1.0" xmlns="http://www.w3.org/2000/svg" width="200pt" height="200pt" viewBox="0 0 200 200" fill="context-fill" fill-opacity="context-fill-opacity"><path d="M118.083 16.167c-7.533 1.183-15.1 6.366-18.816 12.883l-.717 1.267-.283-.617c-1-2.117-4.55-6.317-5.85-6.933-1.517-.734-3.25.316-3.25 1.95 0 .9.183 1.183 1.75 2.7 2.966 2.883 4.883 7 5.666 12.166.134.917.25 2.184.25 2.834l-.016 1.166-1.117.5c-1.367.617-2.767 1.95-3.45 3.3-.367.7-.617.984-.917 1.05-.233.05-1.383.4-2.583.767-2.85.867-5.967 2.383-8.75 4.25-1.233.817-3.033 1.933-4 2.467-3.883 2.166-6.833 4.4-10.1 7.666C58.317 71.117 53.717 80.55 52.317 91.5c-.284 2.133-.267 7.7.016 10.25.634 5.733 2.234 11.133 4.817 16.2 1.85 3.65 3.467 6.05 6.533 9.733 5.784 6.934 10.917 15.7 13.267 22.634l.633 1.85 21.3-.034 21.3-.05.467-1.416c2.233-6.6 7.25-15.417 12.333-21.634.7-.85 1.884-2.316 2.65-3.25 5.45-6.683 8.834-15.033 9.817-24.083.333-3.217.217-8.5-.283-11.733-2.184-14.483-10.734-26.767-23.717-34.117-1-.55-2.233-1.35-2.75-1.733-1.4-1.083-4.233-2.75-6.033-3.567-1.467-.65-4.284-1.633-5.967-2.05-.617-.15-.75-.283-1.133-1.033-.6-1.183-2.05-2.65-3.234-3.283l-1-.534v-1.533a23.86 23.86 0 0 0-.433-4.333l-.1-.45h1.317c2.767 0 6.783-1.034 9.733-2.5 5.3-2.634 10.25-7.55 12.25-12.15.75-1.734 1.233-3.65 1.233-4.867v-.967l-1.116-.35c-1.3-.416-4.5-.583-6.134-.333zM83.533 68.35c.217.083.584.433.8.767.384.55.417.766.4 2.366 0 1.567-.066 1.934-.516 3.15-.867 2.25-2.034 4.017-4.45 6.684-3.384 3.716-4.967 7.4-4.967 11.516.017 1.35.1 2.284.333 3.184.5 1.95.1 3.316-1.367 4.6-2.1 1.85-4.15 1.15-5.616-1.867-1.134-2.333-1.384-3.583-1.384-6.833-.016-2.534.034-3.017.434-4.617 1.016-4 3.416-8.817 6.283-12.55 1.15-1.5 3.633-4.033 4.783-4.85 2.117-1.517 4.084-2.1 5.267-1.55zm44.317 39.267c1.183.616 1.85 1.55 1.95 2.783.1 1.15-.183 1.967-.95 2.817-.75.833-1.417 1.116-2.55 1.116-2.233-.016-3.817-1.783-3.6-4.033.167-1.733 1.783-3.1 3.65-3.133.333 0 .983.2 1.5.45zm4.117 8.916c2.116 1.084 2.483 4.084.683 5.717-.867.783-1.35.967-2.55.933-1.233-.016-2.183-.533-2.867-1.55-.416-.633-.483-.866-.483-1.983 0-1.1.067-1.35.45-1.95 1.083-1.617 2.983-2.083 4.767-1.167zm-9.834 3.5c1.417.667 2.2 2.4 1.8 3.95-.4 1.534-1.3 2.4-2.783 2.717-1.3.25-2.183-.033-3.167-1.017-1.416-1.4-1.533-3.366-.283-4.783 1.183-1.333 2.75-1.65 4.433-.867zm31.684-87.183c-.467.5-1.567 1.717-2.45 2.717s-2.484 2.766-3.55 3.933c-2.434 2.683-2.6 3.133-1.984 5.15.5 1.633.834 1.717 2.467.567 1.45-1.034 3.883-3.467 5.067-5.05 2.25-3.05 2.966-4.584 2.966-6.334 0-1.05-.333-1.533-1.216-1.766-.417-.1-.567 0-1.3.783zM40.25 33.95c-.05.133-.083.6-.083 1.033s-.05.85-.117.9c-.2.217-.55-.266-.667-.916-.116-.75-.483-1-.816-.55-.35.483-.3 2.683.1 3.95.416 1.383 1.216 2.583 3.333 5.05 4.317 5.016 10.717 11.833 11.35 12.1.65.25 1.2.016 2.617-1.167.833-.683 1.033-.967 1.033-1.333 0-.717-2.55-4.25-7.417-10.284-2.733-3.4-4.2-5.366-5.316-7.183-.95-1.533-1.134-1.667-2.634-1.75-1.016-.067-1.3-.033-1.383.15zm133.5 24.733c-.267.084-3.533.684-7.25 1.334-8.15 1.416-8.833 1.55-9.667 1.983-.7.367-1.05.767-1.35 1.633-.233.683-.066 1.9.334 2.383l.3.367 1.816-.283c1-.15 3.584-.417 5.734-.6 4.7-.4 6.15-.583 7.95-1 2.5-.583 3.6-1.35 4.383-3.05.767-1.683.767-1.717.117-2.367-.617-.616-1.284-.733-2.367-.4zm-148.917.434c-.983.166-2.516.766-2.666 1.05-.25.466-.2 1.383.1 1.8.133.2.7.666 1.25 1.033.533.383 2.733 2.05 4.866 3.717 5.517 4.35 7.6 5.916 8.384 6.283 1.35.667 2.7.333 3.633-.883.75-.984.633-1.267-1.1-2.667-.85-.683-2.45-2.1-3.55-3.133-5.767-5.417-7.8-6.884-9.917-7.184a4.07 4.07 0 0 0-1-.016zM161.967 85.65c-3.467.117-3.534.15-3.8 1.767-.35 2.15-.184 2.866.75 3.133.9.267 6.366.817 10.366 1.033 5.467.317 7.534.5 9.934.917 1.033.183 2.05.333 2.25.333.25 0 .683-.3 1.2-.833.45-.467.883-.833.983-.833.333 0 .183-.3-.567-1.1-.416-.434-.75-.85-.75-.934 0-.233.567-.15 1.134.2.733.45 1.033.434 1.033-.066 0-.9-2.65-2.817-4.517-3.267-1.65-.4-10.716-.567-18.016-.35zM23.5 90.433c-4.483.584-5.367.884-6.817 2.234-.65.616-.716.75-.633 1.283.183 1.1-.15 1.05 6.933 1.05 7.734 0 7.5.05 8.917-2.083.483-.734.55-1.284.183-1.634-.55-.566-6.366-1.133-8.583-.85zm54.2 76.434c.05 5.666.083 6.3.383 7.133.4 1.15 5.617 9 6.6 9.95.984.933 2.5 1.817 3.8 2.217 1 .316 1.584.333 10.367.333h9.3l1.3-.417c1.317-.416 2.683-1.2 3.7-2.15.717-.666 5.417-7.683 6.05-9.016.85-1.85.967-2.8.967-8.8v-5.45H77.65l.05 6.2z"/></svg>
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="200pt" height="200pt" viewBox="0 0 200 200" fill="context-fill light-dark(black, white)" fill-opacity="context-fill-opacity"><path d="M118.083 16.167c-7.533 1.183-15.1 6.366-18.816 12.883l-.717 1.267-.283-.617c-1-2.117-4.55-6.317-5.85-6.933-1.517-.734-3.25.316-3.25 1.95 0 .9.183 1.183 1.75 2.7 2.966 2.883 4.883 7 5.666 12.166.134.917.25 2.184.25 2.834l-.016 1.166-1.117.5c-1.367.617-2.767 1.95-3.45 3.3-.367.7-.617.984-.917 1.05-.233.05-1.383.4-2.583.767-2.85.867-5.967 2.383-8.75 4.25-1.233.817-3.033 1.933-4 2.467-3.883 2.166-6.833 4.4-10.1 7.666C58.317 71.117 53.717 80.55 52.317 91.5c-.284 2.133-.267 7.7.016 10.25.634 5.733 2.234 11.133 4.817 16.2 1.85 3.65 3.467 6.05 6.533 9.733 5.784 6.934 10.917 15.7 13.267 22.634l.633 1.85 21.3-.034 21.3-.05.467-1.416c2.233-6.6 7.25-15.417 12.333-21.634.7-.85 1.884-2.316 2.65-3.25 5.45-6.683 8.834-15.033 9.817-24.083.333-3.217.217-8.5-.283-11.733-2.184-14.483-10.734-26.767-23.717-34.117-1-.55-2.233-1.35-2.75-1.733-1.4-1.083-4.233-2.75-6.033-3.567-1.467-.65-4.284-1.633-5.967-2.05-.617-.15-.75-.283-1.133-1.033-.6-1.183-2.05-2.65-3.234-3.283l-1-.534v-1.533a23.86 23.86 0 0 0-.433-4.333l-.1-.45h1.317c2.767 0 6.783-1.034 9.733-2.5 5.3-2.634 10.25-7.55 12.25-12.15.75-1.734 1.233-3.65 1.233-4.867v-.967l-1.116-.35c-1.3-.416-4.5-.583-6.134-.333zM83.533 68.35c.217.083.584.433.8.767.384.55.417.766.4 2.366 0 1.567-.066 1.934-.516 3.15-.867 2.25-2.034 4.017-4.45 6.684-3.384 3.716-4.967 7.4-4.967 11.516.017 1.35.1 2.284.333 3.184.5 1.95.1 3.316-1.367 4.6-2.1 1.85-4.15 1.15-5.616-1.867-1.134-2.333-1.384-3.583-1.384-6.833-.016-2.534.034-3.017.434-4.617 1.016-4 3.416-8.817 6.283-12.55 1.15-1.5 3.633-4.033 4.783-4.85 2.117-1.517 4.084-2.1 5.267-1.55zm44.317 39.267c1.183.616 1.85 1.55 1.95 2.783.1 1.15-.183 1.967-.95 2.817-.75.833-1.417 1.116-2.55 1.116-2.233-.016-3.817-1.783-3.6-4.033.167-1.733 1.783-3.1 3.65-3.133.333 0 .983.2 1.5.45zm4.117 8.916c2.116 1.084 2.483 4.084.683 5.717-.867.783-1.35.967-2.55.933-1.233-.016-2.183-.533-2.867-1.55-.416-.633-.483-.866-.483-1.983 0-1.1.067-1.35.45-1.95 1.083-1.617 2.983-2.083 4.767-1.167zm-9.834 3.5c1.417.667 2.2 2.4 1.8 3.95-.4 1.534-1.3 2.4-2.783 2.717-1.3.25-2.183-.033-3.167-1.017-1.416-1.4-1.533-3.366-.283-4.783 1.183-1.333 2.75-1.65 4.433-.867zm31.684-87.183c-.467.5-1.567 1.717-2.45 2.717s-2.484 2.766-3.55 3.933c-2.434 2.683-2.6 3.133-1.984 5.15.5 1.633.834 1.717 2.467.567 1.45-1.034 3.883-3.467 5.067-5.05 2.25-3.05 2.966-4.584 2.966-6.334 0-1.05-.333-1.533-1.216-1.766-.417-.1-.567 0-1.3.783zM40.25 33.95c-.05.133-.083.6-.083 1.033s-.05.85-.117.9c-.2.217-.55-.266-.667-.916-.116-.75-.483-1-.816-.55-.35.483-.3 2.683.1 3.95.416 1.383 1.216 2.583 3.333 5.05 4.317 5.016 10.717 11.833 11.35 12.1.65.25 1.2.016 2.617-1.167.833-.683 1.033-.967 1.033-1.333 0-.717-2.55-4.25-7.417-10.284-2.733-3.4-4.2-5.366-5.316-7.183-.95-1.533-1.134-1.667-2.634-1.75-1.016-.067-1.3-.033-1.383.15zm133.5 24.733c-.267.084-3.533.684-7.25 1.334-8.15 1.416-8.833 1.55-9.667 1.983-.7.367-1.05.767-1.35 1.633-.233.683-.066 1.9.334 2.383l.3.367 1.816-.283c1-.15 3.584-.417 5.734-.6 4.7-.4 6.15-.583 7.95-1 2.5-.583 3.6-1.35 4.383-3.05.767-1.683.767-1.717.117-2.367-.617-.616-1.284-.733-2.367-.4zm-148.917.434c-.983.166-2.516.766-2.666 1.05-.25.466-.2 1.383.1 1.8.133.2.7.666 1.25 1.033.533.383 2.733 2.05 4.866 3.717 5.517 4.35 7.6 5.916 8.384 6.283 1.35.667 2.7.333 3.633-.883.75-.984.633-1.267-1.1-2.667-.85-.683-2.45-2.1-3.55-3.133-5.767-5.417-7.8-6.884-9.917-7.184a4.07 4.07 0 0 0-1-.016zM161.967 85.65c-3.467.117-3.534.15-3.8 1.767-.35 2.15-.184 2.866.75 3.133.9.267 6.366.817 10.366 1.033 5.467.317 7.534.5 9.934.917 1.033.183 2.05.333 2.25.333.25 0 .683-.3 1.2-.833.45-.467.883-.833.983-.833.333 0 .183-.3-.567-1.1-.416-.434-.75-.85-.75-.934 0-.233.567-.15 1.134.2.733.45 1.033.434 1.033-.066 0-.9-2.65-2.817-4.517-3.267-1.65-.4-10.716-.567-18.016-.35zM23.5 90.433c-4.483.584-5.367.884-6.817 2.234-.65.616-.716.75-.633 1.283.183 1.1-.15 1.05 6.933 1.05 7.734 0 7.5.05 8.917-2.083.483-.734.55-1.284.183-1.634-.55-.566-6.366-1.133-8.583-.85zm54.2 76.434c.05 5.666.083 6.3.383 7.133.4 1.15 5.617 9 6.6 9.95.984.933 2.5 1.817 3.8 2.217 1 .316 1.584.333 10.367.333h9.3l1.3-.417c1.317-.416 2.683-1.2 3.7-2.15.717-.666 5.417-7.683 6.05-9.016.85-1.85.967-2.8.967-8.8v-5.45H77.65l.05 6.2z"/></svg>
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/ChromeProfileMigrator.sys.mjs b/browser/components/migration/ChromeProfileMigrator.sys.mjs
index e32417cd04..17aba35e8a 100644
--- a/browser/components/migration/ChromeProfileMigrator.sys.mjs
+++ b/browser/components/migration/ChromeProfileMigrator.sys.mjs
@@ -785,7 +785,8 @@ async function GetBookmarksResource(aProfileFolder, aBrowserKey) {
}
// Import Bookmark Favicons
- MigrationUtils.insertManyFavicons(favicons);
+ MigrationUtils.insertManyFavicons(favicons).catch(console.error);
+
if (gotErrors) {
throw new Error("The migration included errors.");
}
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/MSMigrationUtils.sys.mjs b/browser/components/migration/MSMigrationUtils.sys.mjs
index 8d9a666e66..37dd69bf10 100644
--- a/browser/components/migration/MSMigrationUtils.sys.mjs
+++ b/browser/components/migration/MSMigrationUtils.sys.mjs
@@ -381,7 +381,7 @@ Bookmarks.prototype = {
}
await MigrationUtils.insertManyBookmarksWrapper(bookmarks, aDestFolderGuid);
- MigrationUtils.insertManyFavicons(favicons);
+ MigrationUtils.insertManyFavicons(favicons).catch(console.error);
},
/**
diff --git a/browser/components/migration/MigrationUtils.sys.mjs b/browser/components/migration/MigrationUtils.sys.mjs
index cda3028cc4..90ba6a535e 100644
--- a/browser/components/migration/MigrationUtils.sys.mjs
+++ b/browser/components/migration/MigrationUtils.sys.mjs
@@ -870,36 +870,53 @@ class MigrationUtils {
* Iterates through the favicons, sniffs for a mime type,
* and uses the mime type to properly import the favicon.
*
+ * Note: You may not want to await on the returned promise, especially if by
+ * doing so there's risk of interrupting the migration of more critical
+ * data (e.g. bookmarks).
+ *
* @param {object[]} favicons
* An array of Objects with these properties:
* {Uint8Array} faviconData: The binary data of a favicon
* {nsIURI} uri: The URI of the associated page
*/
- insertManyFavicons(favicons) {
+ async insertManyFavicons(favicons) {
let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance(
Ci.nsIContentSniffer
);
+
for (let faviconDataItem of favicons) {
- let mimeType = sniffer.getMIMETypeFromContent(
- null,
- faviconDataItem.faviconData,
- faviconDataItem.faviconData.length
- );
+ let dataURL;
+
+ try {
+ // getMIMETypeFromContent throws error if could not get the mime type
+ // from the data.
+ let mimeType = sniffer.getMIMETypeFromContent(
+ null,
+ faviconDataItem.faviconData,
+ faviconDataItem.faviconData.length
+ );
+
+ dataURL = await new Promise((resolve, reject) => {
+ let buffer = new Uint8ClampedArray(faviconDataItem.faviconData);
+ let blob = new Blob([buffer], { type: mimeType });
+ let reader = new FileReader();
+ reader.addEventListener("load", () => resolve(reader.result));
+ reader.addEventListener("error", reject);
+ reader.readAsDataURL(blob);
+ });
+ } catch (e) {
+ // Even if error happens for favicon, continue the process.
+ console.warn(e);
+ continue;
+ }
+
let fakeFaviconURI = Services.io.newURI(
"fake-favicon-uri:" + faviconDataItem.uri.spec
);
- lazy.PlacesUtils.favicons.replaceFaviconData(
- fakeFaviconURI,
- faviconDataItem.faviconData,
- mimeType
- );
- lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
+ lazy.PlacesUtils.favicons.setFaviconForPage(
faviconDataItem.uri,
fakeFaviconURI,
- true,
- lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
- null,
- Services.scriptSecurityManager.getSystemPrincipal()
+ Services.io.newURI(dataURL)
);
}
}
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/SafariProfileMigrator.sys.mjs b/browser/components/migration/SafariProfileMigrator.sys.mjs
index c134c0869a..307edbd230 100644
--- a/browser/components/migration/SafariProfileMigrator.sys.mjs
+++ b/browser/components/migration/SafariProfileMigrator.sys.mjs
@@ -98,7 +98,7 @@ Bookmarks.prototype = {
let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
dbPath,
"Safari favicons",
- `SELECT I.uuid, I.url AS favicon_url, P.url
+ `SELECT I.uuid, I.url AS favicon_url, P.url
FROM icon_info I
INNER JOIN page_url P ON I.uuid = P.uuid;`
);
@@ -253,7 +253,7 @@ Bookmarks.prototype = {
parentGuid
);
- MigrationUtils.insertManyFavicons(favicons);
+ MigrationUtils.insertManyFavicons(favicons).catch(console.error);
},
/**
diff --git a/browser/components/migration/content/migration-wizard.mjs b/browser/components/migration/content/migration-wizard.mjs
index 6fc7a715d7..9d58fbe95f 100644
--- a/browser/components/migration/content/migration-wizard.mjs
+++ b/browser/components/migration/content/migration-wizard.mjs
@@ -333,6 +333,7 @@ export class MigrationWizard extends HTMLElement {
this.#getPermissionsButton.addEventListener("click", this);
this.#browserProfileSelector.addEventListener("click", this);
+ this.#browserProfileSelector.addEventListener("mousedown", this);
this.#resourceTypeList = shadow.querySelector("#resource-type-list");
this.#resourceTypeList.addEventListener("change", this);
@@ -583,9 +584,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(
@@ -1391,107 +1397,122 @@ export class MigrationWizard extends HTMLElement {
}
}
+ #handleClickEvent(event) {
+ if (
+ event.target == this.#importButton ||
+ event.target == this.#importFromFileButton
+ ) {
+ this.#doImport();
+ } else if (
+ event.target.classList.contains("cancel-close") ||
+ event.target.classList.contains("finish-button")
+ ) {
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:Close", { bubbles: true })
+ );
+ } else if (
+ event.currentTarget == this.#browserProfileSelectorList &&
+ event.target != this.#browserProfileSelectorList
+ ) {
+ this.#onBrowserProfileSelectionChanged(event.target);
+ // If the user selected a file migration type from the selector, we'll
+ // help the user out by immediately starting the file migration flow,
+ // rather than waiting for them to click the "Select File".
+ if (
+ event.target.getAttribute("type") ==
+ MigrationWizardConstants.MIGRATOR_TYPES.FILE
+ ) {
+ this.#doImport();
+ }
+ } else if (event.target == this.#safariPermissionButton) {
+ this.#requestSafariPermissions();
+ } else if (event.currentTarget == this.#resourceSummary) {
+ this.#expandedDetails = true;
+ } else if (event.target == this.#chooseImportFromFile) {
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:RequestState", {
+ bubbles: true,
+ detail: {
+ allowOnlyFileMigrators: true,
+ },
+ })
+ );
+ } else if (event.target == this.#safariPasswordImportSkipButton) {
+ // If the user chose to skip importing passwords from Safari, we
+ // programmatically uncheck the PASSWORDS resource type and re-request
+ // import.
+ let checkbox = this.#shadowRoot.querySelector(
+ `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"]`
+ ).control;
+ checkbox.checked = false;
+
+ // If there are no other checked checkboxes, go back to the selection
+ // screen.
+ let checked = this.#shadowRoot.querySelectorAll(
+ `label[data-resource-type] > input:checked`
+ ).length;
+
+ if (!checked) {
+ this.requestState();
+ } else {
+ this.#doImport();
+ }
+ } else if (event.target == this.#safariPasswordImportSelectButton) {
+ this.#selectSafariPasswordFile();
+ } else if (event.target == this.#extensionsSuccessLink) {
+ this.dispatchEvent(
+ new CustomEvent("MigrationWizard:OpenAboutAddons", {
+ bubbles: true,
+ })
+ );
+ event.preventDefault();
+ } else if (event.target == this.#getPermissionsButton) {
+ this.#getPermissions();
+ }
+ }
+
+ #handleChangeEvent(event) {
+ if (event.target == this.#browserProfileSelector) {
+ this.#onBrowserProfileSelectionChanged();
+ } else if (event.target == this.#selectAllCheckbox) {
+ let checkboxes = this.#shadowRoot.querySelectorAll(
+ 'label[data-resource-type]:not([hidden]) > input[type="checkbox"]'
+ );
+ for (let checkbox of checkboxes) {
+ checkbox.checked = this.#selectAllCheckbox.checked;
+ }
+ this.#displaySelectedResources();
+ } else {
+ let checkboxes = this.#shadowRoot.querySelectorAll(
+ 'label[data-resource-type]:not([hidden]) > input[type="checkbox"]'
+ );
+
+ let allVisibleChecked = Array.from(checkboxes).every(checkbox => {
+ return checkbox.checked;
+ });
+
+ this.#selectAllCheckbox.checked = allVisibleChecked;
+ this.#displaySelectedResources();
+ }
+ }
+
handleEvent(event) {
+ if (
+ event.target == this.#browserProfileSelector &&
+ (event.type == "mousedown" ||
+ (event.type == "click" &&
+ event.mozInputSource == MouseEvent.MOZ_SOURCE_KEYBOARD))
+ ) {
+ this.#browserProfileSelectorList.toggle(event);
+ return;
+ }
switch (event.type) {
case "click": {
- if (
- event.target == this.#importButton ||
- event.target == this.#importFromFileButton
- ) {
- this.#doImport();
- } else if (
- event.target.classList.contains("cancel-close") ||
- event.target.classList.contains("finish-button")
- ) {
- this.dispatchEvent(
- new CustomEvent("MigrationWizard:Close", { bubbles: true })
- );
- } else if (event.target == this.#browserProfileSelector) {
- this.#browserProfileSelectorList.show(event);
- } else if (
- event.currentTarget == this.#browserProfileSelectorList &&
- event.target != this.#browserProfileSelectorList
- ) {
- this.#onBrowserProfileSelectionChanged(event.target);
- // If the user selected a file migration type from the selector, we'll
- // help the user out by immediately starting the file migration flow,
- // rather than waiting for them to click the "Select File".
- if (
- event.target.getAttribute("type") ==
- MigrationWizardConstants.MIGRATOR_TYPES.FILE
- ) {
- this.#doImport();
- }
- } else if (event.target == this.#safariPermissionButton) {
- this.#requestSafariPermissions();
- } else if (event.currentTarget == this.#resourceSummary) {
- this.#expandedDetails = true;
- } else if (event.target == this.#chooseImportFromFile) {
- this.dispatchEvent(
- new CustomEvent("MigrationWizard:RequestState", {
- bubbles: true,
- detail: {
- allowOnlyFileMigrators: true,
- },
- })
- );
- } else if (event.target == this.#safariPasswordImportSkipButton) {
- // If the user chose to skip importing passwords from Safari, we
- // programmatically uncheck the PASSWORDS resource type and re-request
- // import.
- let checkbox = this.#shadowRoot.querySelector(
- `label[data-resource-type="${MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS}"]`
- ).control;
- checkbox.checked = false;
-
- // If there are no other checked checkboxes, go back to the selection
- // screen.
- let checked = this.#shadowRoot.querySelectorAll(
- `label[data-resource-type] > input:checked`
- ).length;
-
- if (!checked) {
- this.requestState();
- } else {
- this.#doImport();
- }
- } else if (event.target == this.#safariPasswordImportSelectButton) {
- this.#selectSafariPasswordFile();
- } else if (event.target == this.#extensionsSuccessLink) {
- this.dispatchEvent(
- new CustomEvent("MigrationWizard:OpenAboutAddons", {
- bubbles: true,
- })
- );
- event.preventDefault();
- } else if (event.target == this.#getPermissionsButton) {
- this.#getPermissions();
- }
+ this.#handleClickEvent(event);
break;
}
case "change": {
- if (event.target == this.#browserProfileSelector) {
- this.#onBrowserProfileSelectionChanged();
- } else if (event.target == this.#selectAllCheckbox) {
- let checkboxes = this.#shadowRoot.querySelectorAll(
- 'label[data-resource-type]:not([hidden]) > input[type="checkbox"]'
- );
- for (let checkbox of checkboxes) {
- checkbox.checked = this.#selectAllCheckbox.checked;
- }
- this.#displaySelectedResources();
- } else {
- let checkboxes = this.#shadowRoot.querySelectorAll(
- 'label[data-resource-type]:not([hidden]) > input[type="checkbox"]'
- );
-
- let allVisibleChecked = Array.from(checkboxes).every(checkbox => {
- return checkbox.checked;
- });
-
- this.#selectAllCheckbox.checked = allVisibleChecked;
- this.#displaySelectedResources();
- }
+ this.#handleChangeEvent(event);
break;
}
}
diff --git a/browser/components/migration/tests/browser/browser_disabled_migrator.js b/browser/components/migration/tests/browser/browser_disabled_migrator.js
index 782666f6a6..a9a6b3083c 100644
--- a/browser/components/migration/tests/browser/browser_disabled_migrator.js
+++ b/browser/components/migration/tests/browser/browser_disabled_migrator.js
@@ -17,7 +17,7 @@ add_task(async function test_enabled_migrator() {
let wizard = dialog.querySelector("migration-wizard");
let shadow = wizard.openOrClosedShadowRoot;
let selector = shadow.querySelector("#browser-profile-selector");
- selector.click();
+ EventUtils.synthesizeMouseAtCenter(selector, {}, prefsWin);
await new Promise(resolve => {
shadow
@@ -78,7 +78,7 @@ add_task(async function test_disabling_migrator() {
let wizard = dialog.querySelector("migration-wizard");
let shadow = wizard.openOrClosedShadowRoot;
let selector = shadow.querySelector("#browser-profile-selector");
- selector.click();
+ EventUtils.synthesizeMouseAtCenter(selector, {}, prefsWin);
await new Promise(resolve => {
shadow
diff --git a/browser/components/migration/tests/browser/browser_do_migration.js b/browser/components/migration/tests/browser/browser_do_migration.js
index fab9641960..74454c0ab1 100644
--- a/browser/components/migration/tests/browser/browser_do_migration.js
+++ b/browser/components/migration/tests/browser/browser_do_migration.js
@@ -106,7 +106,7 @@ add_task(async function test_successful_migrations() {
);
let dialogClosed = BrowserTestUtils.waitForEvent(dialog, "close");
- doneButton.click();
+ EventUtils.synthesizeMouseAtCenter(doneButton, {}, prefsWin);
await dialogClosed;
assertQuantitiesShown(wizard, [
MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
diff --git a/browser/components/migration/tests/browser/browser_file_migration.js b/browser/components/migration/tests/browser/browser_file_migration.js
index c73dfc4456..94f4ff2908 100644
--- a/browser/components/migration/tests/browser/browser_file_migration.js
+++ b/browser/components/migration/tests/browser/browser_file_migration.js
@@ -132,7 +132,7 @@ add_task(async function test_file_migration() {
// Now select our DummyFileMigrator from the list.
let selector = shadow.querySelector("#browser-profile-selector");
- selector.click();
+ EventUtils.synthesizeMouseAtCenter(selector, {}, prefsWin);
info("Waiting for panel-list shown");
await new Promise(resolve => {
@@ -246,7 +246,7 @@ add_task(async function test_file_migration_error() {
// Now select our DummyFileMigrator from the list.
let selector = shadow.querySelector("#browser-profile-selector");
- selector.click();
+ EventUtils.synthesizeMouseAtCenter(selector, {}, prefsWin);
info("Waiting for panel-list shown");
await new Promise(resolve => {
diff --git a/browser/components/migration/tests/browser/head.js b/browser/components/migration/tests/browser/head.js
index d3d188a7e1..8824a50ee9 100644
--- a/browser/components/migration/tests/browser/head.js
+++ b/browser/components/migration/tests/browser/head.js
@@ -332,7 +332,7 @@ async function selectResourceTypesAndStartMigration(
// First, select the InternalTestingProfileMigrator browser.
let selector = shadow.querySelector("#browser-profile-selector");
- selector.click();
+ EventUtils.synthesizeMouseAtCenter(selector, {}, wizard.ownerGlobal);
await new Promise(resolve => {
shadow
diff --git a/browser/components/migration/tests/chrome/test_migration_wizard.html b/browser/components/migration/tests/chrome/test_migration_wizard.html
index cc2d8a0363..43fd3ab931 100644
--- a/browser/components/migration/tests/chrome/test_migration_wizard.html
+++ b/browser/components/migration/tests/chrome/test_migration_wizard.html
@@ -147,7 +147,7 @@
// Test that the resource type checkboxes are shown or hidden depending on
// which resourceTypes are included with the MigratorProfileInstance.
for (let migratorInstance of MIGRATOR_PROFILE_INSTANCES) {
- selector.click();
+ synthesizeMouseAtCenter(selector, {}, gWiz.ownerGlobal);
await new Promise(resolve => {
gShadowRoot
.querySelector("panel-list")
@@ -248,7 +248,7 @@
ok(isHidden(preamble), "preamble should be hidden.");
let selector = gShadowRoot.querySelector("#browser-profile-selector");
- selector.click();
+ synthesizeMouseAtCenter(selector, {}, gWiz.ownerGlobal);
await new Promise(resolve => {
let panelList = gShadowRoot.querySelector("panel-list");
if (panelList) {
diff --git a/browser/components/migration/tests/unit/head_migration.js b/browser/components/migration/tests/unit/head_migration.js
index 9900f34232..9b056e6670 100644
--- a/browser/components/migration/tests/unit/head_migration.js
+++ b/browser/components/migration/tests/unit/head_migration.js
@@ -118,6 +118,34 @@ async function assertFavicons(pageURIs) {
}
/**
+ * Check the image data for favicon of given page uri.
+ *
+ * @param {string} pageURI
+ * The page URI to which the favicon belongs.
+ * @param {Array} expectedImageData
+ * Expected image data of the favicon.
+ * @param {string} expectedMimeType
+ * Expected mime type of the favicon.
+ */
+async function assertFavicon(pageURI, expectedImageData, expectedMimeType) {
+ let result = await new Promise(resolve => {
+ PlacesUtils.favicons.getFaviconDataForPage(
+ Services.io.newURI(pageURI),
+ (faviconURI, dataLen, imageData, mimeType) => {
+ resolve({ faviconURI, dataLen, imageData, mimeType });
+ }
+ );
+ });
+ Assert.ok(!!result, `Got favicon for ${pageURI}`);
+ Assert.equal(
+ result.imageData.join(","),
+ expectedImageData.join(","),
+ "Image data is correct"
+ );
+ Assert.equal(result.mimeType, expectedMimeType, "Mime type is correct");
+}
+
+/**
* Replaces a directory service entry with a given nsIFile.
*
* @param {string} key
diff --git a/browser/components/migration/tests/unit/test_Chrome_bookmarks.js b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
index d115cda412..3c09869800 100644
--- a/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
@@ -71,11 +71,13 @@ async function testBookmarks(migratorKey, subDirs) {
).path;
await IOUtils.copy(sourcePath, target.path);
- // Get page url for each favicon
- let faviconURIs = await MigrationUtils.getRowsFromDBWithoutLocks(
+ // Get page url and the image data for each favicon
+ let favicons = await MigrationUtils.getRowsFromDBWithoutLocks(
sourcePath,
"Chrome Bookmark Favicons",
- `select page_url from icon_mapping`
+ `SELECT page_url, image_data FROM icon_mapping
+ INNER JOIN favicon_bitmaps ON (favicon_bitmaps.icon_id = icon_mapping.icon_id)
+ `
);
target.append("Bookmarks");
@@ -171,10 +173,14 @@ async function testBookmarks(migratorKey, subDirs) {
"Telemetry reporting correct."
);
Assert.ok(observerNotified, "The observer should be notified upon migration");
- let pageUrls = Array.from(faviconURIs, f =>
- Services.io.newURI(f.getResultByName("page_url"))
- );
- await assertFavicons(pageUrls);
+
+ for (const favicon of favicons) {
+ await assertFavicon(
+ favicon.getResultByName("page_url"),
+ favicon.getResultByName("image_data"),
+ "image/png"
+ );
+ }
}
add_task(async function test_Chrome() {
diff --git a/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js b/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js
index 2578353e35..a22e6e1655 100644
--- a/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js
+++ b/browser/components/migration/tests/unit/test_Safari_history_strange_entries.js
@@ -74,7 +74,7 @@ add_task(async function testHistoryImportStrangeEntries() {
await PlacesUtils.history.clear();
let placesQuery = new PlacesQuery();
- let emptyHistory = await placesQuery.getHistory();
+ let emptyHistory = await placesQuery.getHistory({ daysOld: Infinity });
Assert.equal(emptyHistory.size, 0, "Empty history should indeed be empty.");
const EXPECTED_MIGRATED_SITES = 10;
@@ -94,7 +94,10 @@ add_task(async function testHistoryImportStrangeEntries() {
let migrator = await MigrationUtils.getMigrator("safari");
await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
- let migratedHistory = await placesQuery.getHistory({ sortBy: "site" });
+ let migratedHistory = await placesQuery.getHistory({
+ daysOld: Infinity,
+ sortBy: "site",
+ });
let siteCount = migratedHistory.size;
let visitCount = 0;
for (let [, visits] of migratedHistory) {
diff --git a/browser/components/moz.build b/browser/components/moz.build
index 0f91b90fb0..1909ee2786 100644
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -67,6 +67,7 @@ DIRS += [
"tabpreview",
"tabunloader",
"textrecognition",
+ "topsites",
"translations",
"uitour",
"urlbar",
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/AboutNewTabService.sys.mjs b/browser/components/newtab/AboutNewTabService.sys.mjs
index 73502fcb4f..37adc25f6e 100644
--- a/browser/components/newtab/AboutNewTabService.sys.mjs
+++ b/browser/components/newtab/AboutNewTabService.sys.mjs
@@ -109,7 +109,11 @@ export const AboutHomeStartupCacheChild = {
);
}
- if (!lazy.NimbusFeatures.abouthomecache.getVariable("enabled")) {
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.startup.homepage.abouthome_cache.enabled"
+ )
+ ) {
return;
}
diff --git a/browser/components/newtab/common/Actions.sys.mjs b/browser/components/newtab/common/Actions.mjs
index df5c9f0c91..a86a1d1e81 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,12 @@ for (const type of [
"UPDATE_PINNED_SEARCH_SHORTCUTS",
"UPDATE_SEARCH_SHORTCUTS",
"UPDATE_SECTION_PREFS",
+ "WALLPAPERS_SET",
+ "WALLPAPER_CLICK",
+ "WEATHER_IMPRESSION",
+ "WEATHER_LOAD_ERROR",
+ "WEATHER_OPEN_PROVIDER_URL",
+ "WEATHER_UPDATE",
"WEBEXT_CLICK",
"WEBEXT_DISMISS",
]) {
@@ -371,8 +379,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..edd3668434 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,15 @@ export const INITIAL_STATE = {
// Hide the search box after handing off to AwesomeBar and user starts typing.
hide: false,
},
+ Wallpapers: {
+ wallpaperList: [],
+ },
+ Weather: {
+ // do we have the data from WeatherFeed yet?
+ initialized: false,
+ suggestions: [],
+ lastUpdated: null,
+ },
};
function App(prevState = INITIAL_STATE.App, action) {
@@ -841,6 +850,29 @@ 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;
+ }
+}
+
+function Weather(prevState = INITIAL_STATE.Weather, action) {
+ switch (action.type) {
+ case at.WEATHER_UPDATE:
+ return {
+ ...prevState,
+ suggestions: action.data.suggestions,
+ lastUpdated: action.data.date,
+ initialized: true,
+ };
+ default:
+ return prevState;
+ }
+}
+
export const reducers = {
TopSites,
App,
@@ -852,4 +884,6 @@ export const reducers = {
Personalization,
DiscoveryStream,
Search,
+ Wallpapers,
+ Weather,
};
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..61722fd418 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";
@@ -15,6 +12,10 @@ import { CustomizeMenu } from "content-src/components/CustomizeMenu/CustomizeMen
import React from "react";
import { Search } from "content-src/components/Search/Search";
import { Sections } from "content-src/components/Sections/Sections";
+import { Weather } from "content-src/components/Weather/Weather";
+
+const VISIBLE = "visible";
+const VISIBILITY_CHANGE_EVENT = "visibilitychange";
export const PrefsButton = ({ onClick, icon }) => (
<div className="prefs-button">
@@ -76,7 +77,7 @@ export class _Base extends React.PureComponent {
]
.filter(v => v)
.join(" ");
- global.document.body.className = bodyClassName;
+ globalThis.document.body.className = bodyClassName;
}
render() {
@@ -110,17 +111,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 +219,89 @@ 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.string}
+ 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 || ""})`
+ );
+
+ // Add helper class to body if user has a wallpaper selected
+ if (lightWallpaper) {
+ global.document?.body.classList.add("hasWallpaperLight");
+ }
+
+ if (darkWallpaper) {
+ global.document?.body.classList.add("hasWallpaperDark");
+ }
+ }
+ }
+
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 weatherEnabled = prefs.showWeather;
+
const { pocketConfig } = prefs;
const isDiscoveryStream =
@@ -196,10 +333,12 @@ export class BaseContent extends React.PureComponent {
showSponsoredPocketEnabled: prefs.showSponsored,
showRecentSavesEnabled: prefs.showRecentSaves,
topSitesRowsCount: prefs.topSitesRows,
+ weatherEnabled: prefs.showWeather,
};
const pocketRegion = prefs["feeds.system.topstories"];
const mayHaveSponsoredStories = prefs["system.showSponsored"];
+ const mayHaveWeather = prefs["system.showWeather"];
const { mayHaveSponsoredTopSites } = prefs;
const outerClassName = [
@@ -215,6 +354,9 @@ export class BaseContent extends React.PureComponent {
]
.filter(v => v)
.join(" ");
+ if (wallpapersEnabled) {
+ this.updateWallpaper();
+ }
return (
<div>
@@ -224,9 +366,12 @@ export class BaseContent extends React.PureComponent {
openPreferences={this.openPreferences}
setPref={this.setPref}
enabledSections={enabledSections}
+ wallpapersEnabled={wallpapersEnabled}
+ activeWallpaper={activeWallpaper}
pocketRegion={pocketRegion}
mayHaveSponsoredTopSites={mayHaveSponsoredTopSites}
mayHaveSponsoredStories={mayHaveSponsoredStories}
+ mayHaveWeather={mayHaveWeather}
spocMessageVariant={spocMessageVariant}
showing={customizeMenuVisible}
/>
@@ -252,6 +397,7 @@ export class BaseContent extends React.PureComponent {
<DiscoveryStreamBase
locale={props.App.locale}
mayHaveSponsoredStories={mayHaveSponsoredStories}
+ firstVisibleTimestamp={this.state.firstVisibleTimestamp}
/>
</ErrorBoundary>
) : (
@@ -259,17 +405,31 @@ export class BaseContent extends React.PureComponent {
)}
</div>
<ConfirmDialog />
+ {wallpapersEnabled && this.renderWallpaperAttribution()}
</main>
+ <aside>
+ {weatherEnabled && (
+ <ErrorBoundary>
+ <Weather />
+ </ErrorBoundary>
+ )}
+ </aside>
</div>
</div>
);
}
}
+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,
+ Weather: state.Weather,
}))(_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..494d506da9 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) {
@@ -27,7 +28,7 @@ export class ContentSection extends React.PureComponent {
}
onPreferenceSelect(e) {
- // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS
+ // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS | WEATHER
const { preference, eventSource } = e.target.dataset;
let value;
if (e.target.nodeName === "SELECT") {
@@ -96,13 +97,18 @@ export class ContentSection extends React.PureComponent {
pocketRegion,
mayHaveSponsoredStories,
mayHaveRecentSaves,
+ mayHaveWeather,
openPreferences,
spocMessageVariant,
+ wallpapersEnabled,
+ activeWallpaper,
+ setPref,
} = this.props;
const {
topSitesEnabled,
pocketEnabled,
highlightsEnabled,
+ weatherEnabled,
showSponsoredTopSitesEnabled,
showSponsoredPocketEnabled,
showRecentSavesEnabled,
@@ -111,6 +117,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"
@@ -256,6 +271,22 @@ export class ContentSection extends React.PureComponent {
</label>
</div>
+ {mayHaveWeather && (
+ <div id="weather-section" className="section">
+ <label className="switch">
+ <moz-toggle
+ id="weather-toggle"
+ pressed={weatherEnabled || null}
+ onToggle={this.onPreferenceSelect}
+ data-preference="showWeather"
+ data-eventSource="WEATHER"
+ data-l10n-id="newtab-custom-weather-toggle"
+ data-l10n-attrs="label, description"
+ />
+ </label>
+ </div>
+ )}
+
{pocketRegion &&
mayHaveSponsoredStories &&
spocMessageVariant === "variant-c" && (
diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx b/browser/components/newtab/content-src/components/CustomizeMenu/CustomizeMenu.jsx
index 54dcd550c4..035e84af58 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";
@@ -56,21 +55,25 @@ export class _CustomizeMenu extends React.PureComponent {
role="dialog"
data-l10n-id="newtab-personalize-dialog-label"
>
- <button
- onClick={() => this.props.onClose()}
- className="close-button"
- data-l10n-id="newtab-custom-close-button"
- ref={c => (this.closeButton = c)}
- />
- <BackgroundsSection />
+ <div className="close-button-wrapper">
+ <button
+ onClick={() => this.props.onClose()}
+ className="close-button"
+ data-l10n-id="newtab-custom-close-button"
+ ref={c => (this.closeButton = c)}
+ />
+ </div>
<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}
mayHaveRecentSaves={this.props.DiscoveryStream.recentSavesEnabled}
+ mayHaveWeather={this.props.mayHaveWeather}
spocMessageVariant={this.props.spocMessageVariant}
dispatch={this.props.dispatch}
/>
diff --git a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss
index 579e455a3f..403a62a50f 100644
--- a/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss
+++ b/browser/components/newtab/content-src/components/CustomizeMenu/_CustomizeMenu.scss
@@ -47,7 +47,8 @@
inset-block: 0;
inset-inline-end: 0;
z-index: 1001;
- padding: 16px;
+ padding-block: 0 var(--space-large);
+ padding-inline: var(--space-large);
overflow: auto;
transform: translateX(435px);
visibility: hidden;
@@ -85,9 +86,17 @@
box-shadow: $shadow-large;
}
+ .close-button-wrapper {
+ position: sticky;
+ top: 0;
+ padding-block-start: var(--space-large);
+ background-color: var(--newtab-background-color-secondary);
+ z-index: 1;
+ }
+
.close-button {
margin-inline-start: auto;
- margin-bottom: 28px;
+ margin-inline-end: var(--space-large);
white-space: nowrap;
display: block;
background-color: var(--newtab-element-secondary-color);
@@ -117,7 +126,11 @@
grid-template-columns: 1fr;
grid-template-rows: repeat(4, auto);
grid-row-gap: 32px;
- padding: 0 16px;
+ padding: var(--space-large);
+
+ .wallpapers-section h2 {
+ font-size: inherit;
+ }
.section {
moz-toggle {
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.jsx
index 3c31a5a29f..79d453a7c9 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";
@@ -129,8 +126,11 @@ export class DiscoveryStreamAdminUI extends React.PureComponent {
this.systemTick = this.systemTick.bind(this);
this.syncRemoteSettings = this.syncRemoteSettings.bind(this);
this.onStoryToggle = this.onStoryToggle.bind(this);
+ this.handleWeatherSubmit = this.handleWeatherSubmit.bind(this);
+ this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this);
this.state = {
toggledStories: {},
+ weatherQuery: "",
};
}
@@ -185,6 +185,16 @@ export class DiscoveryStreamAdminUI extends React.PureComponent {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SYNC_RS);
}
+ handleWeatherUpdate(e) {
+ this.setState({ weatherQuery: e.target.value || "" });
+ }
+
+ handleWeatherSubmit(e) {
+ e.preventDefault();
+ const { weatherQuery } = this.state;
+ this.props.dispatch(ac.SetPref("weather.query", weatherQuery));
+ }
+
renderComponent(width, component) {
return (
<table>
@@ -203,6 +213,46 @@ export class DiscoveryStreamAdminUI extends React.PureComponent {
);
}
+ renderWeatherData() {
+ const { suggestions } = this.props.state.Weather;
+ let weatherTable;
+ if (suggestions) {
+ weatherTable = (
+ <div className="weather-section">
+ <form onSubmit={this.handleWeatherSubmit}>
+ <label htmlFor="weather-query">Weather query</label>
+ <input
+ type="text"
+ min="3"
+ max="10"
+ id="weather-query"
+ onChange={this.handleWeatherUpdate}
+ value={this.weatherQuery}
+ />
+ <button type="submit">Submit</button>
+ </form>
+ <table>
+ <tbody>
+ {suggestions.map(suggestion => (
+ <tr className="message-item" key={suggestion.city_name}>
+ <td className="message-id">
+ <span>
+ {suggestion.city_name} <br />
+ </span>
+ </td>
+ <td className="message-summary">
+ <pre>{JSON.stringify(suggestion, null, 2)}</pre>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+ return weatherTable;
+ }
+
renderFeedData(url) {
const { feeds } = this.props.state.DiscoveryStream;
const feed = feeds.data[url].data;
@@ -379,6 +429,8 @@ export class DiscoveryStreamAdminUI extends React.PureComponent {
{this.renderSpocs()}
<h3>Feeds Data</h3>
{this.renderFeedsData()}
+ <h3>Weather Data</h3>
+ {this.renderWeatherData()}
</div>
);
}
@@ -415,6 +467,7 @@ export class DiscoveryStreamAdminInner extends React.PureComponent {
state={{
DiscoveryStream: this.props.DiscoveryStream,
Personalization: this.props.Personalization,
+ Weather: this.props.Weather,
}}
otherPrefs={this.props.Prefs.values}
dispatch={this.props.dispatch}
@@ -445,9 +498,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 +513,7 @@ export class CollapseToggle extends React.PureComponent {
}
componentWillUnmount() {
- global.document.body.classList.remove("no-scroll");
+ globalThis.document.body.classList.remove("no-scroll");
}
render() {
@@ -503,4 +556,5 @@ export const DiscoveryStreamAdmin = connect(state => ({
DiscoveryStream: state.DiscoveryStream,
Personalization: state.Personalization,
Prefs: state.Prefs,
+ Weather: state.Weather,
}))(_DiscoveryStreamAdmin);
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss
index a01227dd3d..dcad97c917 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamAdmin/DiscoveryStreamAdmin.scss
@@ -334,4 +334,16 @@
}
}
}
+
+ .weather-section {
+ margin-block-end: 24px;
+
+ form {
+ display: flex;
+
+ label {
+ margin-inline-end: 12px;
+ }
+ }
+ }
}
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..461d54899f 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" && (
@@ -465,57 +468,34 @@ export class _DSCard extends React.PureComponent {
dispatch={this.props.dispatch}
spocMessageVariant={this.props.spocMessageVariant}
/>
- {saveToPocketCard && (
- <div className="card-stp-button-hover-background">
- <div className="card-stp-button-position-wrapper">
- {!this.props.flightId && stpButton()}
- <DSLinkMenu
- id={this.props.id}
- index={this.props.pos}
- dispatch={this.props.dispatch}
- url={this.props.url}
- title={this.props.title}
- source={source}
- type={this.props.type}
- pocket_id={this.props.pocket_id}
- shim={this.props.shim}
- bookmarkGuid={this.props.bookmarkGuid}
- flightId={
- !this.props.is_collection ? this.props.flightId : undefined
- }
- showPrivacyInfo={!!this.props.flightId}
- onMenuUpdate={this.onMenuUpdate}
- onMenuShow={this.onMenuShow}
- saveToPocketCard={saveToPocketCard}
- pocket_button_enabled={pocketButtonEnabled}
- isRecentSave={isRecentSave}
- />
- </div>
+
+ <div className="card-stp-button-hover-background">
+ <div className="card-stp-button-position-wrapper">
+ {saveToPocketCard && <>{!this.props.flightId && stpButton()}</>}
+
+ <DSLinkMenu
+ id={this.props.id}
+ index={this.props.pos}
+ dispatch={this.props.dispatch}
+ url={this.props.url}
+ title={this.props.title}
+ source={source}
+ type={this.props.type}
+ pocket_id={this.props.pocket_id}
+ shim={this.props.shim}
+ bookmarkGuid={this.props.bookmarkGuid}
+ flightId={
+ !this.props.is_collection ? this.props.flightId : undefined
+ }
+ showPrivacyInfo={!!this.props.flightId}
+ onMenuUpdate={this.onMenuUpdate}
+ onMenuShow={this.onMenuShow}
+ saveToPocketCard={saveToPocketCard}
+ pocket_button_enabled={pocketButtonEnabled}
+ isRecentSave={isRecentSave}
+ />
</div>
- )}
- {!saveToPocketCard && (
- <DSLinkMenu
- id={this.props.id}
- index={this.props.pos}
- dispatch={this.props.dispatch}
- url={this.props.url}
- title={this.props.title}
- source={source}
- type={this.props.type}
- pocket_id={this.props.pocket_id}
- shim={this.props.shim}
- bookmarkGuid={this.props.bookmarkGuid}
- flightId={
- !this.props.is_collection ? this.props.flightId : undefined
- }
- showPrivacyInfo={!!this.props.flightId}
- hostRef={this.contextMenuButtonHostRef}
- onMenuUpdate={this.onMenuUpdate}
- onMenuShow={this.onMenuShow}
- pocket_button_enabled={pocketButtonEnabled}
- isRecentSave={isRecentSave}
- />
- )}
+ </div>
</article>
);
}
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
index 9004e609df..e5ac19b553 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss
@@ -54,12 +54,6 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%);
fill: $white;
}
- .context-menu-button {
- position: static;
- transition: none;
- border-radius: 3px;
- }
-
.context-menu-position-container {
position: relative;
}
@@ -83,6 +77,10 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%);
padding: 6px;
white-space: nowrap;
color: $white;
+
+ &:focus-visible {
+ outline: 2px solid var(--newtab-button-focus-border);
+ }
}
button,
@@ -95,6 +93,28 @@ $ds-card-image-gradient-solid: rgba(0, 0, 0, 100%);
}
}
+ // Override note: The colors set here are intentionally static
+ // due to transparency issues over images.
+ .context-menu-button {
+ position: static;
+ transition: none;
+ border-radius: 3px;
+ background-color: var(--newtab-button-static-background);
+
+ &:hover {
+ background-color: var(--newtab-button-static-hover-background);
+
+ &:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-static-focus-background);
+ }
+ }
+
&.last-item {
.card-stp-button-hover-background {
.context-menu {
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/Highlights/_Highlights.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss
index 54b39524d8..f726e936b9 100644
--- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss
+++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Highlights/_Highlights.scss
@@ -37,6 +37,24 @@
a {
text-decoration: none;
}
+
+ // Override note: The colors set here are intentionally static
+ // due to transparency issues over images.
+ .context-menu-button {
+ background-color: var(--newtab-button-static-background);
+
+ &:hover {
+ background-color: var(--newtab-button-static-hover-background);
+
+ &:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+ }
+
+ &:focus-visible {
+ background-color: var(--newtab-button-static-focus-background);
+ }
+ }
}
}
}
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/TopSites/_TopSites.scss b/browser/components/newtab/content-src/components/TopSites/_TopSites.scss
index 4e4019513d..09a20c235d 100644
--- a/browser/components/newtab/content-src/components/TopSites/_TopSites.scss
+++ b/browser/components/newtab/content-src/components/TopSites/_TopSites.scss
@@ -126,12 +126,23 @@ $letter-fallback-color: $white;
}
}
+ // Necessary for when navigating by a keyboard, having the context
+ // menu open should display the "…" button. This style is a clone
+ // of the `:active` state for `.context-menu-button`
+ &.active {
+ .context-menu-button {
+ opacity: 1;
+ background-color: var(--newtab-button-active-background);
+ }
+ }
+
.context-menu-button {
background-image: url('chrome://global/skin/icons/more.svg');
+ background-color: var(--newtab-button-background);
border: 0;
border-radius: 4px;
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ fill: var(--newtab-button-text);
-moz-context-properties: fill;
height: 20px;
width: 20px;
@@ -141,11 +152,18 @@ $letter-fallback-color: $white;
top: -20px;
transition: opacity 200ms;
- &:is(:active, :focus) {
- outline: 0;
+ &:hover {
+ background-color: var(--newtab-button-hover-background);
+
+ &:active {
+ background-color: var(--newtab-button-active-background);
+ }
+ }
+
+ &:focus-visible {
+ background-color: var(--newtab-button-focus-background);
+ border-color: var(--newtab-button-focus-border);
opacity: 1;
- background-color: var(--newtab-element-hover-color);
- fill: var(--newtab-primary-action-background);
}
}
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..6fcd4b3a15
--- /dev/null
+++ b/browser/components/newtab/content-src/components/WallpapersSection/WallpapersSection.jsx
@@ -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/. */
+
+import React from "react";
+import { connect } from "react-redux";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
+
+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);
+ this.handleUserEvent({
+ selected_wallpaper: id,
+ hadPreviousWallpaper: !!this.props.activeWallpaper,
+ });
+ // 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}`, "");
+ this.handleUserEvent({
+ selected_wallpaper: "none",
+ hadPreviousWallpaper: !!this.props.activeWallpaper,
+ });
+ }
+
+ // Record user interaction when changing wallpaper and reseting wallpaper to default
+ handleUserEvent(data) {
+ this.props.dispatch(
+ ac.OnlyToMain({
+ type: at.WALLPAPER_CLICK,
+ data,
+ })
+ );
+ }
+
+ 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/components/Weather/Weather.jsx b/browser/components/newtab/content-src/components/Weather/Weather.jsx
new file mode 100644
index 0000000000..9273f9a4bd
--- /dev/null
+++ b/browser/components/newtab/content-src/components/Weather/Weather.jsx
@@ -0,0 +1,350 @@
+/* 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 { connect } from "react-redux";
+import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
+import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
+import React from "react";
+
+const VISIBLE = "visible";
+const VISIBILITY_CHANGE_EVENT = "visibilitychange";
+
+export class _Weather extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ contextMenuKeyboard: false,
+ showContextMenu: false,
+ url: "https://example.com",
+ impressionSeen: false,
+ errorSeen: false,
+ };
+ this.setImpressionRef = element => {
+ this.impressionElement = element;
+ };
+ this.setErrorRef = element => {
+ this.errorElement = element;
+ };
+ this.onClick = this.onClick.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onUpdate = this.onUpdate.bind(this);
+ this.onProviderClick = this.onProviderClick.bind(this);
+ }
+
+ componentDidMount() {
+ const { props } = this;
+
+ if (!props.dispatch) {
+ return;
+ }
+
+ if (props.document.visibilityState === VISIBLE) {
+ // Setup the impression observer once the page is visible.
+ this.setImpressionObservers();
+ } else {
+ // We should only ever send the latest impression stats ping, so remove any
+ // older listeners.
+ if (this._onVisibilityChange) {
+ props.document.removeEventListener(
+ VISIBILITY_CHANGE_EVENT,
+ this._onVisibilityChange
+ );
+ }
+
+ this._onVisibilityChange = () => {
+ if (props.document.visibilityState === VISIBLE) {
+ // Setup the impression observer once the page is visible.
+ this.setImpressionObservers();
+ props.document.removeEventListener(
+ VISIBILITY_CHANGE_EVENT,
+ this._onVisibilityChange
+ );
+ }
+ };
+ props.document.addEventListener(
+ VISIBILITY_CHANGE_EVENT,
+ this._onVisibilityChange
+ );
+ }
+ }
+
+ componentWillUnmount() {
+ // Remove observers on unmount
+ if (this.observer && this.impressionElement) {
+ this.observer.unobserve(this.impressionElement);
+ }
+ if (this.observer && this.errorElement) {
+ this.observer.unobserve(this.errorElement);
+ }
+ if (this._onVisibilityChange) {
+ this.props.document.removeEventListener(
+ VISIBILITY_CHANGE_EVENT,
+ this._onVisibilityChange
+ );
+ }
+ }
+
+ setImpressionObservers() {
+ if (this.impressionElement) {
+ this.observer = new IntersectionObserver(this.onImpression.bind(this));
+ this.observer.observe(this.impressionElement);
+ }
+ if (this.errorElement) {
+ this.observer = new IntersectionObserver(this.onError.bind(this));
+ this.observer.observe(this.errorElement);
+ }
+ }
+
+ onImpression(entries) {
+ if (this.state) {
+ const entry = entries.find(e => e.isIntersecting);
+
+ if (entry) {
+ if (this.impressionElement) {
+ this.observer.unobserve(this.impressionElement);
+ }
+
+ this.props.dispatch(
+ ac.OnlyToMain({
+ type: at.WEATHER_IMPRESSION,
+ })
+ );
+
+ // Stop observing since element has been seen
+ this.setState({
+ impressionSeen: true,
+ });
+ }
+ }
+ }
+
+ onError(entries) {
+ if (this.state) {
+ const entry = entries.find(e => e.isIntersecting);
+
+ if (entry) {
+ if (this.errorElement) {
+ this.observer.unobserve(this.errorElement);
+ }
+
+ this.props.dispatch(
+ ac.OnlyToMain({
+ type: at.WEATHER_LOAD_ERROR,
+ })
+ );
+
+ // Stop observing since element has been seen
+ this.setState({
+ errorSeen: true,
+ });
+ }
+ }
+ }
+
+ openContextMenu(isKeyBoard) {
+ if (this.props.onUpdate) {
+ this.props.onUpdate(true);
+ }
+ this.setState({
+ showContextMenu: true,
+ contextMenuKeyboard: isKeyBoard,
+ });
+ }
+
+ onClick(event) {
+ event.preventDefault();
+ this.openContextMenu(false, event);
+ }
+
+ onKeyDown(event) {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ this.openContextMenu(true, event);
+ }
+ }
+
+ onUpdate(showContextMenu) {
+ if (this.props.onUpdate) {
+ this.props.onUpdate(showContextMenu);
+ }
+ this.setState({ showContextMenu });
+ }
+
+ onProviderClick() {
+ this.props.dispatch(
+ ac.OnlyToMain({
+ type: at.WEATHER_OPEN_PROVIDER_URL,
+ data: {
+ source: "WEATHER",
+ },
+ })
+ );
+ }
+
+ render() {
+ // Check if weather should be rendered
+ const isWeatherEnabled = this.props.Prefs.values["system.showWeather"];
+
+ if (!isWeatherEnabled || !this.props.Weather.initialized) {
+ return false;
+ }
+
+ const { showContextMenu } = this.state;
+
+ const WEATHER_SUGGESTION = this.props.Weather.suggestions?.[0];
+
+ const {
+ className,
+ index,
+ dispatch,
+ eventSource,
+ shouldSendImpressionStats,
+ } = this.props;
+ const { props } = this;
+ const isContextMenuOpen = this.state.activeCard === index;
+
+ const outerClassName = [
+ "weather",
+ className,
+ isContextMenuOpen && "active",
+ props.placeholder && "placeholder",
+ ]
+ .filter(v => v)
+ .join(" ");
+
+ const showDetailedView =
+ this.props.Prefs.values["weather.display"] === "detailed";
+
+ // Note: The temperature units/display options will become secondary menu items
+ const WEATHER_SOURCE_CONTEXT_MENU_OPTIONS = [
+ ...(this.props.Prefs.values["weather.locationSearchEnabled"]
+ ? ["ChangeWeatherLocation"]
+ : []),
+ ...(this.props.Prefs.values["weather.temperatureUnits"] === "f"
+ ? ["ChangeTempUnitCelsius"]
+ : ["ChangeTempUnitFahrenheit"]),
+ ...(this.props.Prefs.values["weather.display"] === "simple"
+ ? ["ChangeWeatherDisplayDetailed"]
+ : ["ChangeWeatherDisplaySimple"]),
+ "HideWeather",
+ "OpenLearnMoreURL",
+ ];
+
+ // Only return the widget if we have data. Otherwise, show error state
+ if (WEATHER_SUGGESTION) {
+ return (
+ <div ref={this.setImpressionRef} className={outerClassName}>
+ <div className="weatherCard">
+ <a
+ data-l10n-id="newtab-weather-see-forecast"
+ data-l10n-args='{"provider": "AccuWeather"}'
+ href={WEATHER_SUGGESTION.forecast.url}
+ className="weatherInfoLink"
+ onClick={this.onProviderClick}
+ >
+ <div className="weatherIconCol">
+ <span
+ className={`weatherIcon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}`}
+ />
+ </div>
+ <div className="weatherText">
+ <div className="weatherForecastRow">
+ <span className="weatherTemperature">
+ {
+ WEATHER_SUGGESTION.current_conditions.temperature[
+ this.props.Prefs.values["weather.temperatureUnits"]
+ ]
+ }
+ &deg;{this.props.Prefs.values["weather.temperatureUnits"]}
+ </span>
+ </div>
+ <div className="weatherCityRow">
+ <span className="weatherCity">
+ {WEATHER_SUGGESTION.city_name}
+ </span>
+ </div>
+ {showDetailedView ? (
+ <div className="weatherDetailedSummaryRow">
+ <div className="weatherHighLowTemps">
+ {/* Low Forecasted Temperature */}
+ <span>
+ {
+ WEATHER_SUGGESTION.forecast.high[
+ this.props.Prefs.values["weather.temperatureUnits"]
+ ]
+ }
+ &deg;
+ {this.props.Prefs.values["weather.temperatureUnits"]}
+ </span>
+ {/* Spacer / Bullet */}
+ <span>&bull;</span>
+ {/* Low Forecasted Temperature */}
+ <span>
+ {
+ WEATHER_SUGGESTION.forecast.low[
+ this.props.Prefs.values["weather.temperatureUnits"]
+ ]
+ }
+ &deg;
+ {this.props.Prefs.values["weather.temperatureUnits"]}
+ </span>
+ </div>
+ <span className="weatherTextSummary">
+ {WEATHER_SUGGESTION.current_conditions.summary}
+ </span>
+ </div>
+ ) : null}
+ </div>
+ </a>
+ <div className="weatherButtonContextMenuWrapper">
+ <button
+ aria-haspopup="true"
+ onKeyDown={this.onKeyDown}
+ onClick={this.onClick}
+ data-l10n-id="newtab-menu-section-tooltip"
+ className="weatherButtonContextMenu"
+ >
+ {showContextMenu ? (
+ <LinkMenu
+ dispatch={dispatch}
+ index={index}
+ source={eventSource}
+ onUpdate={this.onUpdate}
+ options={WEATHER_SOURCE_CONTEXT_MENU_OPTIONS}
+ site={{
+ url: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page",
+ }}
+ link="https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page"
+ shouldSendImpressionStats={shouldSendImpressionStats}
+ />
+ ) : null}
+ </button>
+ </div>
+ </div>
+ <span
+ data-l10n-id="newtab-weather-sponsored"
+ data-l10n-args='{"provider": "AccuWeather"}'
+ className="weatherSponsorText"
+ ></span>
+ </div>
+ );
+ }
+
+ return (
+ <div ref={this.setErrorRef} className={outerClassName}>
+ <div className="weatherNotAvailable">
+ <span className="icon icon-small-spacer icon-info-critical" />{" "}
+ <span data-l10n-id="newtab-weather-error-not-available"></span>
+ </div>
+ </div>
+ );
+ }
+}
+
+export const Weather = connect(state => ({
+ Weather: state.Weather,
+ Prefs: state.Prefs,
+ IntersectionObserver: globalThis.IntersectionObserver,
+ document: globalThis.document,
+}))(_Weather);
diff --git a/browser/components/newtab/content-src/components/Weather/_Weather.scss b/browser/components/newtab/content-src/components/Weather/_Weather.scss
new file mode 100644
index 0000000000..0616530f98
--- /dev/null
+++ b/browser/components/newtab/content-src/components/Weather/_Weather.scss
@@ -0,0 +1,393 @@
+// Custom font sizing for weather widget
+:root {
+ --newtab-weather-content-font-size: 11px;
+ --newtab-weather-sponsor-font-size: 8px;
+}
+
+.weather {
+ font-size: var(--font-size-root);
+ position: absolute;
+ left: var(--space-xlarge);
+ top: var(--space-xlarge);
+ z-index: 1;
+}
+
+// Unavailable / Error State
+.weatherNotAvailable {
+ font-size: var(--newtab-weather-content-font-size);
+ color: var(--text-color-error);
+ display: flex;
+ align-items: center;
+
+ .icon {
+ fill: var(--icon-color-critical);
+ -moz-context-properties: fill;
+ }
+}
+
+.weatherCard {
+ margin-block-end: var(--space-xsmall);
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: stretch;
+ border-radius: var(--border-radius-medium);
+ overflow: hidden;
+
+ &:hover, &:focus-within {
+ ~ .weatherSponsorText {
+ visibility: visible;
+ }
+ }
+
+ &:focus-within {
+ overflow: visible;
+ }
+
+ &:hover {
+ box-shadow: var(--box-shadow-10);
+ background: var(--background-color-box);
+ }
+
+ a {
+ color: var(--text-color);
+ }
+
+}
+
+.weatherSponsorText {
+ visibility: hidden;
+ font-size: var(--newtab-weather-sponsor-font-size);
+ color: var(--text-color-deemphasized);
+}
+
+.weatherInfoLink, .weatherButtonContextMenuWrapper {
+ appearance: none;
+ background-color: var(--background-color-ghost);
+ border: 0;
+ padding: var(--space-small);
+ cursor: pointer;
+
+ &:hover {
+ // TODO: Add Wallpaper Background Color Fix
+ background-color: var(--button-background-color-ghost-hover);
+
+ &::after {
+ background-color: transparent
+ }
+
+ &:active {
+ // TODO: Add Wallpaper Background Color Fix
+ background-color: var(--button-background-color-ghost-active);
+ }
+ }
+
+ &:focus-visible {
+ outline: var(--focus-outline);
+ }
+
+ // Contrast fix for users who have wallpapers set
+ .hasWallpaperDark & {
+ @media (prefers-color-scheme: dark) {
+ // TODO: Replace with token
+ background-color: rgba(35, 34, 43, 70%);
+
+ &:hover {
+ background-color: var(--newtab-button-static-hover-background);
+ }
+
+ &:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+ }
+
+ @media (prefers-contrast) and (prefers-color-scheme: dark) {
+ background-color: var(--background-color-box);
+ }
+ }
+
+ .hasWallpaperLight & {
+ @media (prefers-color-scheme: light) {
+ // TODO: Replace with token
+ background-color: rgba(255, 255, 255, 70%);
+
+ &:hover {
+ background-color: var(--newtab-button-static-hover-background);
+ }
+
+ &:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+ }
+
+ @media (prefers-contrast) and (prefers-color-scheme: light) {
+ background-color: var(--background-color-box);
+ }
+ }
+
+}
+
+.weatherInfoLink {
+ display: flex;
+ gap: var(--space-medium);
+ padding: var(--space-small) var(--space-medium);
+ border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium);
+ text-decoration: none;
+ color: var(--text-color);;
+ min-width: 130px;
+ max-width: 190px;
+ text-overflow: ellipsis;
+
+ @media(min-width: $break-point-medium) {
+ min-width: unset;
+ }
+
+ &:hover ~.weatherButtonContextMenuWrapper {
+ &::after {
+ background-color: transparent
+ }
+ }
+
+ &:focus-visible {
+ border-radius: var(--border-radius-medium);
+
+ ~ .weatherButtonContextMenuWrapper {
+ &::after {
+ background-color: transparent
+ }
+ }
+ }
+}
+
+.weatherButtonContextMenuWrapper {
+ position: relative;
+ cursor: pointer;
+ border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0;
+ display: flex;
+ align-items: stretch;
+ width: 50px;
+ padding: 0;
+
+ &::after {
+ content: '';
+ left: 0;
+ top: 10px;
+ height: calc(100% - 20px);
+ width: 1px;
+ background-color: var(--newtab-button-static-background);
+ display: block;
+ position: absolute;
+ z-index: 0;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ &::after {
+ background-color: var(--color-gray-70);
+ }
+ }
+
+ &:hover {
+ &::after {
+ background-color: transparent
+ }
+ }
+
+ &:focus-visible {
+ border-radius: var(--border-radius-medium);
+
+ &::after {
+ background-color: transparent
+ }
+ }
+}
+
+.weatherButtonContextMenu {
+ background-image: url('chrome://global/skin/icons/more.svg');
+ background-repeat: no-repeat;
+ background-size: var(--size-item-small) auto;
+ background-position: center;
+ background-color: transparent;
+ cursor: pointer;
+ fill: var(--icon-color);
+ -moz-context-properties: fill;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ appearance: none;
+ min-width: var(--size-item-large);
+}
+
+.weatherText {
+ height: min-content;
+}
+
+.weatherCityRow, .weatherForecastRow, .weatherDetailedSummaryRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-small);
+}
+
+.weatherForecastRow {
+ text-transform: uppercase;
+ font-weight: var(--font-weight-bold);
+}
+
+.weatherCityRow {
+ color: var(--text-color-deemphasized);
+}
+
+.weatherCity {
+ text-overflow: ellipsis;
+ font-size: var(--font-size-small);
+}
+
+// Add additional margin if detailed summary is in view
+.weatherCityRow + .weatherDetailedSummaryRow {
+ margin-block-start: var(--space-xsmall);
+}
+
+.weatherDetailedSummaryRow {
+ font-size: var(--newtab-weather-content-font-size);
+ gap: var(--space-large);
+}
+
+.weatherHighLowTemps {
+ display: flex;
+ gap: var(--space-xxsmall);
+ text-transform: uppercase;
+ word-spacing: var(--space-xxsmall);
+}
+
+.weatherTextSummary {
+ text-align: center;
+ max-width: 90px;
+}
+
+.weatherTemperature {
+ font-size: var(--font-size-large);
+}
+
+// Weather Symbol Icons
+.weatherIconCol {
+ width: var(--size-item-large);
+ height: var(--size-item-large);
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ align-self: center;
+}
+
+.weatherIcon {
+ width: var(--size-item-large);
+ height: auto;
+ vertical-align: middle;
+
+ @media (prefers-contrast) {
+ -moz-context-properties: fill, stroke;
+ fill: currentColor;
+ stroke: currentColor;
+ }
+
+ &.iconId1 {
+ content: url('chrome://browser/skin/weather/sunny.svg');
+ // height: var(--size-item-large);
+ }
+
+ &.iconId2 {
+ content: url('chrome://browser/skin/weather/mostly-sunny.svg');
+ // height: var(--size-item-large);
+ }
+
+ &:is(.iconId3, .iconId4, .iconId6) {
+ content: url('chrome://browser/skin/weather/partly-sunny.svg');
+ // height: var(--size-item-large);
+ }
+
+ &.iconId5 {
+ content: url('chrome://browser/skin/weather/hazy-sunshine.svg');
+ // height: var(--size-item-large);
+ }
+
+ &:is(.iconId7, .iconId8) {
+ content: url('chrome://browser/skin/weather/cloudy.svg');
+ }
+
+ &.iconId11 {
+ content: url('chrome://browser/skin/weather/fog.svg');
+ }
+
+ &.iconId12 {
+ content: url('chrome://browser/skin/weather/showers.svg');
+ }
+
+ &:is(.iconId13, .iconId14) {
+ content: url('chrome://browser/skin/weather/mostly-cloudy-with-showers.svg');
+ // height: var(--size-item-large);
+ }
+
+ &.iconId15 {
+ content: url('chrome://browser/skin/weather/thunderstorms.svg');
+ }
+
+ &:is(.iconId16, .iconId17) {
+ content: url('chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg');
+ }
+
+ &.iconId18 {
+ content: url('chrome://browser/skin/weather/rain.svg');
+ }
+
+ &:is(.iconId19, .iconId20, .iconId25) {
+ content: url('chrome://browser/skin/weather/flurries.svg');
+ }
+
+ &.iconId21 {
+ content: url('chrome://browser/skin/weather/partly-sunny-with-flurries.svg');
+ }
+
+ &:is(.iconId22, .iconId23) {
+ content: url('chrome://browser/skin/weather/snow.svg');
+ }
+
+ &:is(.iconId24, .iconId31) {
+ content: url('chrome://browser/skin/weather/ice.svg');
+ }
+
+ &:is(.iconId26, .iconId29) {
+ content: url('chrome://browser/skin/weather/freezing-rain.svg');
+ }
+
+ &.iconId30 {
+ content: url('chrome://browser/skin/weather/hot.svg');
+ }
+
+ &.iconId32 {
+ content: url('chrome://browser/skin/weather/windy.svg');
+ }
+
+ &.iconId33 {
+ content: url('chrome://browser/skin/weather/night-clear.svg');
+ }
+
+ &:is(.iconId34, .iconId35, .iconId36, .iconId38) {
+ content: url('chrome://browser/skin/weather/night-mostly-clear.svg');
+ }
+
+ &.iconId37 {
+ content: url('chrome://browser/skin/weather/night-hazy-moonlight.svg');
+ }
+
+ &:is(.iconId39, .iconId40) {
+ content: url('chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg');
+ height: var(--size-item-large);
+ }
+
+ &:is(.iconId41, .iconId42) {
+ content: url('chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg');
+ }
+
+ &:is(.iconId43, .iconId44) {
+ content: url('chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg');
+ }
+}
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..23dcf8b050 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",
@@ -306,4 +306,68 @@ export const LinkMenuOptions = {
: LinkMenuOptions.EmptyItem(),
OpenInPrivateWindow: (site, index, eventSource, isEnabled) =>
isEnabled ? _OpenInPrivateWindow(site) : LinkMenuOptions.EmptyItem(),
+ ChangeWeatherLocation: () => ({
+ id: "newtab-weather-menu-change-location",
+ action: ac.OnlyToMain({
+ type: at.CHANGE_WEATHER_LOCATION,
+ data: { url: "https://mozilla.org" },
+ }),
+ }),
+ ChangeWeatherDisplaySimple: () => ({
+ id: "newtab-weather-menu-change-weather-display-simple",
+ action: ac.OnlyToMain({
+ type: at.SET_PREF,
+ data: {
+ name: "weather.display",
+ value: "simple",
+ },
+ }),
+ }),
+ ChangeWeatherDisplayDetailed: () => ({
+ id: "newtab-weather-menu-change-weather-display-detailed",
+ action: ac.OnlyToMain({
+ type: at.SET_PREF,
+ data: {
+ name: "weather.display",
+ value: "detailed",
+ },
+ }),
+ }),
+ ChangeTempUnitFahrenheit: () => ({
+ id: "newtab-weather-menu-change-temperature-units-fahrenheit",
+ action: ac.OnlyToMain({
+ type: at.SET_PREF,
+ data: {
+ name: "weather.temperatureUnits",
+ value: "f",
+ },
+ }),
+ }),
+ ChangeTempUnitCelsius: () => ({
+ id: "newtab-weather-menu-change-temperature-units-celsius",
+ action: ac.OnlyToMain({
+ type: at.SET_PREF,
+ data: {
+ name: "weather.temperatureUnits",
+ value: "c",
+ },
+ }),
+ }),
+ HideWeather: () => ({
+ id: "newtab-weather-menu-hide-weather",
+ action: ac.OnlyToMain({
+ type: at.SET_PREF,
+ data: {
+ name: "showWeather",
+ value: false,
+ },
+ }),
+ }),
+ OpenLearnMoreURL: site => ({
+ id: "newtab-weather-menu-learn-more",
+ action: ac.OnlyToMain({
+ type: at.OPEN_LINK,
+ data: { url: site.url },
+ }),
+ }),
};
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..580f35416e 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,8 @@ input {
@import '../components/ContextMenu/ContextMenu';
@import '../components/ConfirmDialog/ConfirmDialog';
@import '../components/CustomizeMenu/CustomizeMenu';
+@import '../components/WallpapersSection/WallpapersSection';
+@import '../components/Weather/Weather';
@import '../components/Card/Card';
@import '../components/CollapsibleSection/CollapsibleSection';
@import '../components/DiscoveryStreamAdmin/DiscoveryStreamAdmin';
diff --git a/browser/components/newtab/content-src/styles/_icons.scss b/browser/components/newtab/content-src/styles/_icons.scss
index 8be97ad9ae..39879b2b44 100644
--- a/browser/components/newtab/content-src/styles/_icons.scss
+++ b/browser/components/newtab/content-src/styles/_icons.scss
@@ -4,7 +4,7 @@
background-size: $icon-size;
-moz-context-properties: fill;
display: inline-block;
- color: var(--newtab-text-primary-color);
+ color: var(--icon-color);
fill: currentColor;
height: $icon-size;
vertical-align: middle;
@@ -70,6 +70,10 @@
background-image: url('chrome://global/skin/icons/info.svg');
}
+ &.icon-info-critical {
+ background-image: url('chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg');
+ }
+
&.icon-help {
background-image: url('chrome://global/skin/icons/help.svg');
}
@@ -167,6 +171,10 @@
background-image: url('chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg');
}
+ &.icon-weather {
+ background-image: url('chrome://browser/skin/weather/sunny.svg');
+ }
+
&.icon-highlights {
background-image: url('chrome://global/skin/icons/highlights.svg');
}
diff --git a/browser/components/newtab/content-src/styles/_theme.scss b/browser/components/newtab/content-src/styles/_theme.scss
index 6b097ae93e..78b54f4f8e 100644
--- a/browser/components/newtab/content-src/styles/_theme.scss
+++ b/browser/components/newtab/content-src/styles/_theme.scss
@@ -38,6 +38,21 @@ $shadow-image-inset: inset 0 0 0 0.5px $black-15;
--newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #{$black});
--newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #{$black});
+ // --newtab-button-*-color is used on all new page card/top site options buttons
+ --newtab-button-background: var(--button-background-color);
+ --newtab-button-focus-background: var(--newtab-button-background);
+ --newtab-button-focus-border: var(--focus-outline-color);
+ --newtab-button-hover-background: var(--button-background-color-hover);
+ --newtab-button-active-background: var(--button-background-color-active);
+ --newtab-button-text: var(--button-text-color);
+
+ // --newtab-button-static*-color is used on pocket cards and require a
+ // static color unit due to transparency issues with `color-mix`
+ --newtab-button-static-background: #F0F0F4;
+ --newtab-button-static-focus-background: var(--newtab-button-static-background);
+ --newtab-button-static-hover-background: #E0E0E6;
+ --newtab-button-static-active-background: #CFCFD8;
+
// --newtab-element-secondary*-color is used when an element needs to be set
// off from the secondary background color.
--newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent);
@@ -87,6 +102,12 @@ $shadow-image-inset: inset 0 0 0 0.5px $black-15;
--newtab-primary-element-text-color: #{$primary-text-color-dark};
--newtab-wordmark-color: #{$newtab-wordmark-darktheme-color};
--newtab-status-success: #{$status-dark-green};
+
+ // --newtab-button-static*-color is used on pocket cards and require a
+ // static color unit due to transparency issues with `color-mix`
+ --newtab-button-static-background: #2B2A33;
+ --newtab-button-static-hover-background: #52525E;
+ --newtab-button-static-active-background: #5B5B66;
}
}
diff --git a/browser/components/newtab/content-src/styles/_variables.scss b/browser/components/newtab/content-src/styles/_variables.scss
index 9fd0083841..43672c7796 100644
--- a/browser/components/newtab/content-src/styles/_variables.scss
+++ b/browser/components/newtab/content-src/styles/_variables.scss
@@ -157,14 +157,17 @@ $customize-menu-border-tint: 1px solid rgba(0, 0, 0, 15%);
@mixin context-menu-button {
.context-menu-button {
background-clip: padding-box;
- background-color: var(--newtab-background-color-secondary);
+ background-color: var(--newtab-button-background);
background-image: url('chrome://global/skin/icons/more.svg');
background-position: 55%;
- border: $border-primary;
+ border: 0;
+ outline: $border-primary;
+ outline-width: 0;
border-radius: 100%;
box-shadow: $context-menu-button-boxshadow;
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ color: var(--button-text-color);
+ fill: var(--newtab-button-text);
height: $context-menu-button-size;
inset-inline-end: math.div(-$context-menu-button-size, 2);
opacity: 0;
@@ -175,10 +178,26 @@ $customize-menu-border-tint: 1px solid rgba(0, 0, 0, 15%);
transition-property: transform, opacity;
width: $context-menu-button-size;
- &:is(:active, :focus) {
+ &:is(:active, :focus-visible, :hover) {
opacity: 1;
transform: scale(1);
}
+
+ &:is(:hover) {
+ background-color: var(--newtab-button-hover-background);
+ }
+
+ &:is(:focus-visible) {
+ outline-color: var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-focus-background);
+ outline-width: 4px;
+ }
+
+ &:is(:active) {
+ background-color: var(--newtab-button-active-background);
+ }
+
+
}
}
diff --git a/browser/components/newtab/css/activity-stream-linux.css b/browser/components/newtab/css/activity-stream-linux.css
index 8773159737..248de6cf21 100644
--- a/browser/components/newtab/css/activity-stream-linux.css
+++ b/browser/components/newtab/css/activity-stream-linux.css
@@ -42,6 +42,16 @@ input {
--newtab-text-secondary-color: color-mix(in srgb, var(--newtab-text-primary-color) 70%, transparent);
--newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #000);
--newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #000);
+ --newtab-button-background: var(--button-background-color);
+ --newtab-button-focus-background: var(--newtab-button-background);
+ --newtab-button-focus-border: var(--focus-outline-color);
+ --newtab-button-hover-background: var(--button-background-color-hover);
+ --newtab-button-active-background: var(--button-background-color-active);
+ --newtab-button-text: var(--button-text-color);
+ --newtab-button-static-background: #F0F0F4;
+ --newtab-button-static-focus-background: var(--newtab-button-static-background);
+ --newtab-button-static-hover-background: #E0E0E6;
+ --newtab-button-static-active-background: #CFCFD8;
--newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent);
--newtab-element-secondary-hover-color: color-mix(in srgb, currentColor 12%, transparent);
--newtab-element-secondary-active-color: color-mix(in srgb, currentColor 25%, transparent);
@@ -79,6 +89,9 @@ input {
--newtab-primary-element-text-color: #2b2a33;
--newtab-wordmark-color: #fbfbfe;
--newtab-status-success: #7C6;
+ --newtab-button-static-background: #2B2A33;
+ --newtab-button-static-hover-background: #52525E;
+ --newtab-button-static-active-background: #5B5B66;
}
@media (prefers-contrast) {
@@ -92,7 +105,7 @@ input {
background-size: 16px;
-moz-context-properties: fill;
display: inline-block;
- color: var(--newtab-text-primary-color);
+ color: var(--icon-color);
fill: currentColor;
height: 16px;
vertical-align: middle;
@@ -142,6 +155,9 @@ input {
.icon.icon-info {
background-image: url("chrome://global/skin/icons/info.svg");
}
+.icon.icon-info-critical {
+ background-image: url("chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg");
+}
.icon.icon-help {
background-image: url("chrome://global/skin/icons/help.svg");
}
@@ -222,6 +238,9 @@ input {
.icon.icon-webextension {
background-image: url("chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg");
}
+.icon.icon-weather {
+ background-image: url("chrome://browser/skin/weather/sunny.svg");
+}
.icon.icon-highlights {
background-image: url("chrome://global/skin/icons/highlights.svg");
}
@@ -276,6 +295,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 +434,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 +524,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;
@@ -620,12 +678,17 @@ main section {
.top-site-outer:is(:hover) .context-menu-button {
opacity: 1;
}
+.top-site-outer.active .context-menu-button {
+ opacity: 1;
+ background-color: var(--newtab-button-active-background);
+}
.top-site-outer .context-menu-button {
background-image: url("chrome://global/skin/icons/more.svg");
+ background-color: var(--newtab-button-background);
border: 0;
border-radius: 4px;
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ fill: var(--newtab-button-text);
-moz-context-properties: fill;
height: 20px;
width: 20px;
@@ -635,11 +698,16 @@ main section {
top: -20px;
transition: opacity 200ms;
}
-.top-site-outer .context-menu-button:is(:active, :focus) {
- outline: 0;
+.top-site-outer .context-menu-button:hover {
+ background-color: var(--newtab-button-hover-background);
+}
+.top-site-outer .context-menu-button:hover:active {
+ background-color: var(--newtab-button-active-background);
+}
+.top-site-outer .context-menu-button:focus-visible {
+ background-color: var(--newtab-button-focus-background);
+ border-color: var(--newtab-button-focus-border);
opacity: 1;
- background-color: var(--newtab-element-hover-color);
- fill: var(--newtab-primary-action-background);
}
.top-site-outer .tile {
border-radius: 8px;
@@ -1636,7 +1704,8 @@ main section {
inset-block: 0;
inset-inline-end: 0;
z-index: 1001;
- padding: 16px;
+ padding-block: 0 var(--space-large);
+ padding-inline: var(--space-large);
overflow: auto;
transform: translateX(435px);
visibility: hidden;
@@ -1663,9 +1732,16 @@ main section {
.customize-menu.customize-animate-exit-active {
box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.2);
}
+.customize-menu .close-button-wrapper {
+ position: sticky;
+ top: 0;
+ padding-block-start: var(--space-large);
+ background-color: var(--newtab-background-color-secondary);
+ z-index: 1;
+}
.customize-menu .close-button {
margin-inline-start: auto;
- margin-bottom: 28px;
+ margin-inline-end: var(--space-large);
white-space: nowrap;
display: block;
background-color: var(--newtab-element-secondary-color);
@@ -1692,7 +1768,10 @@ main section {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, auto);
grid-row-gap: 32px;
- padding: 0 16px;
+ padding: var(--space-large);
+}
+.home-section .wallpapers-section h2 {
+ font-size: inherit;
}
.home-section .section moz-toggle {
margin-bottom: 10px;
@@ -1830,6 +1909,439 @@ 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;
+}
+
+:root {
+ --newtab-weather-content-font-size: 11px;
+ --newtab-weather-sponsor-font-size: 8px;
+}
+
+.weather {
+ font-size: var(--font-size-root);
+ position: absolute;
+ left: var(--space-xlarge);
+ top: var(--space-xlarge);
+ z-index: 1;
+}
+
+.weatherNotAvailable {
+ font-size: var(--newtab-weather-content-font-size);
+ color: var(--text-color-error);
+ display: flex;
+ align-items: center;
+}
+.weatherNotAvailable .icon {
+ fill: var(--icon-color-critical);
+ -moz-context-properties: fill;
+}
+
+.weatherCard {
+ margin-block-end: var(--space-xsmall);
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: stretch;
+ border-radius: var(--border-radius-medium);
+ overflow: hidden;
+}
+.weatherCard:hover ~ .weatherSponsorText, .weatherCard:focus-within ~ .weatherSponsorText {
+ visibility: visible;
+}
+.weatherCard:focus-within {
+ overflow: visible;
+}
+.weatherCard:hover {
+ box-shadow: var(--box-shadow-10);
+ background: var(--background-color-box);
+}
+.weatherCard a {
+ color: var(--text-color);
+}
+
+.weatherSponsorText {
+ visibility: hidden;
+ font-size: var(--newtab-weather-sponsor-font-size);
+ color: var(--text-color-deemphasized);
+}
+
+.weatherInfoLink, .weatherButtonContextMenuWrapper {
+ appearance: none;
+ background-color: var(--background-color-ghost);
+ border: 0;
+ padding: var(--space-small);
+ cursor: pointer;
+}
+.weatherInfoLink:hover, .weatherButtonContextMenuWrapper:hover {
+ background-color: var(--button-background-color-ghost-hover);
+}
+.weatherInfoLink:hover::after, .weatherButtonContextMenuWrapper:hover::after {
+ background-color: transparent;
+}
+.weatherInfoLink:hover:active, .weatherButtonContextMenuWrapper:hover:active {
+ background-color: var(--button-background-color-ghost-active);
+}
+.weatherInfoLink:focus-visible, .weatherButtonContextMenuWrapper:focus-visible {
+ outline: var(--focus-outline);
+}
+@media (prefers-color-scheme: dark) {
+ .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper {
+ background-color: rgba(35, 34, 43, 0.7);
+ }
+ .hasWallpaperDark .weatherInfoLink:hover, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover {
+ background-color: var(--newtab-button-static-hover-background);
+ }
+ .hasWallpaperDark .weatherInfoLink:hover:active, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+}
+@media (prefers-contrast) and (prefers-color-scheme: dark) {
+ .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper {
+ background-color: var(--background-color-box);
+ }
+}
+@media (prefers-color-scheme: light) {
+ .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper {
+ background-color: rgba(255, 255, 255, 0.7);
+ }
+ .hasWallpaperLight .weatherInfoLink:hover, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover {
+ background-color: var(--newtab-button-static-hover-background);
+ }
+ .hasWallpaperLight .weatherInfoLink:hover:active, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+}
+@media (prefers-contrast) and (prefers-color-scheme: light) {
+ .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper {
+ background-color: var(--background-color-box);
+ }
+}
+
+.weatherInfoLink {
+ display: flex;
+ gap: var(--space-medium);
+ padding: var(--space-small) var(--space-medium);
+ border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium);
+ text-decoration: none;
+ color: var(--text-color);
+ min-width: 130px;
+ max-width: 190px;
+ text-overflow: ellipsis;
+}
+@media (min-width: 610px) {
+ .weatherInfoLink {
+ min-width: unset;
+ }
+}
+.weatherInfoLink:hover ~ .weatherButtonContextMenuWrapper::after {
+ background-color: transparent;
+}
+.weatherInfoLink:focus-visible {
+ border-radius: var(--border-radius-medium);
+}
+.weatherInfoLink:focus-visible ~ .weatherButtonContextMenuWrapper::after {
+ background-color: transparent;
+}
+
+.weatherButtonContextMenuWrapper {
+ position: relative;
+ cursor: pointer;
+ border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0;
+ display: flex;
+ align-items: stretch;
+ width: 50px;
+ padding: 0;
+}
+.weatherButtonContextMenuWrapper::after {
+ content: "";
+ left: 0;
+ top: 10px;
+ height: calc(100% - 20px);
+ width: 1px;
+ background-color: var(--newtab-button-static-background);
+ display: block;
+ position: absolute;
+ z-index: 0;
+}
+@media (prefers-color-scheme: dark) {
+ .weatherButtonContextMenuWrapper::after {
+ background-color: var(--color-gray-70);
+ }
+}
+.weatherButtonContextMenuWrapper:hover::after {
+ background-color: transparent;
+}
+.weatherButtonContextMenuWrapper:focus-visible {
+ border-radius: var(--border-radius-medium);
+}
+.weatherButtonContextMenuWrapper:focus-visible::after {
+ background-color: transparent;
+}
+
+.weatherButtonContextMenu {
+ background-image: url("chrome://global/skin/icons/more.svg");
+ background-repeat: no-repeat;
+ background-size: var(--size-item-small) auto;
+ background-position: center;
+ background-color: transparent;
+ cursor: pointer;
+ fill: var(--icon-color);
+ -moz-context-properties: fill;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ appearance: none;
+ min-width: var(--size-item-large);
+}
+
+.weatherText {
+ height: min-content;
+}
+
+.weatherCityRow, .weatherForecastRow, .weatherDetailedSummaryRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-small);
+}
+
+.weatherForecastRow {
+ text-transform: uppercase;
+ font-weight: var(--font-weight-bold);
+}
+
+.weatherCityRow {
+ color: var(--text-color-deemphasized);
+}
+
+.weatherCity {
+ text-overflow: ellipsis;
+ font-size: var(--font-size-small);
+}
+
+.weatherCityRow + .weatherDetailedSummaryRow {
+ margin-block-start: var(--space-xsmall);
+}
+
+.weatherDetailedSummaryRow {
+ font-size: var(--newtab-weather-content-font-size);
+ gap: var(--space-large);
+}
+
+.weatherHighLowTemps {
+ display: flex;
+ gap: var(--space-xxsmall);
+ text-transform: uppercase;
+ word-spacing: var(--space-xxsmall);
+}
+
+.weatherTextSummary {
+ text-align: center;
+ max-width: 90px;
+}
+
+.weatherTemperature {
+ font-size: var(--font-size-large);
+}
+
+.weatherIconCol {
+ width: var(--size-item-large);
+ height: var(--size-item-large);
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ align-self: center;
+}
+
+.weatherIcon {
+ width: var(--size-item-large);
+ height: auto;
+ vertical-align: middle;
+}
+@media (prefers-contrast) {
+ .weatherIcon {
+ -moz-context-properties: fill, stroke;
+ fill: currentColor;
+ stroke: currentColor;
+ }
+}
+.weatherIcon.iconId1 {
+ content: url("chrome://browser/skin/weather/sunny.svg");
+}
+.weatherIcon.iconId2 {
+ content: url("chrome://browser/skin/weather/mostly-sunny.svg");
+}
+.weatherIcon:is(.iconId3, .iconId4, .iconId6) {
+ content: url("chrome://browser/skin/weather/partly-sunny.svg");
+}
+.weatherIcon.iconId5 {
+ content: url("chrome://browser/skin/weather/hazy-sunshine.svg");
+}
+.weatherIcon:is(.iconId7, .iconId8) {
+ content: url("chrome://browser/skin/weather/cloudy.svg");
+}
+.weatherIcon.iconId11 {
+ content: url("chrome://browser/skin/weather/fog.svg");
+}
+.weatherIcon.iconId12 {
+ content: url("chrome://browser/skin/weather/showers.svg");
+}
+.weatherIcon:is(.iconId13, .iconId14) {
+ content: url("chrome://browser/skin/weather/mostly-cloudy-with-showers.svg");
+}
+.weatherIcon.iconId15 {
+ content: url("chrome://browser/skin/weather/thunderstorms.svg");
+}
+.weatherIcon:is(.iconId16, .iconId17) {
+ content: url("chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg");
+}
+.weatherIcon.iconId18 {
+ content: url("chrome://browser/skin/weather/rain.svg");
+}
+.weatherIcon:is(.iconId19, .iconId20, .iconId25) {
+ content: url("chrome://browser/skin/weather/flurries.svg");
+}
+.weatherIcon.iconId21 {
+ content: url("chrome://browser/skin/weather/partly-sunny-with-flurries.svg");
+}
+.weatherIcon:is(.iconId22, .iconId23) {
+ content: url("chrome://browser/skin/weather/snow.svg");
+}
+.weatherIcon:is(.iconId24, .iconId31) {
+ content: url("chrome://browser/skin/weather/ice.svg");
+}
+.weatherIcon:is(.iconId26, .iconId29) {
+ content: url("chrome://browser/skin/weather/freezing-rain.svg");
+}
+.weatherIcon.iconId30 {
+ content: url("chrome://browser/skin/weather/hot.svg");
+}
+.weatherIcon.iconId32 {
+ content: url("chrome://browser/skin/weather/windy.svg");
+}
+.weatherIcon.iconId33 {
+ content: url("chrome://browser/skin/weather/night-clear.svg");
+}
+.weatherIcon:is(.iconId34, .iconId35, .iconId36, .iconId38) {
+ content: url("chrome://browser/skin/weather/night-mostly-clear.svg");
+}
+.weatherIcon.iconId37 {
+ content: url("chrome://browser/skin/weather/night-hazy-moonlight.svg");
+}
+.weatherIcon:is(.iconId39, .iconId40) {
+ content: url("chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg");
+ height: var(--size-item-large);
+}
+.weatherIcon:is(.iconId41, .iconId42) {
+ content: url("chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg");
+}
+.weatherIcon:is(.iconId43, .iconId44) {
+ content: url("chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg");
+}
+
/* stylelint-disable max-nesting-depth */
.card-outer {
background: var(--newtab-background-color-secondary);
@@ -1842,14 +2354,17 @@ main section {
}
.card-outer .context-menu-button {
background-clip: padding-box;
- background-color: var(--newtab-background-color-secondary);
+ background-color: var(--newtab-button-background);
background-image: url("chrome://global/skin/icons/more.svg");
background-position: 55%;
- border: 1px solid var(--newtab-border-color);
+ border: 0;
+ outline: 1px solid var(--newtab-border-color);
+ outline-width: 0;
border-radius: 100%;
box-shadow: 0 2px rgba(12, 12, 13, 0.1);
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ color: var(--button-text-color);
+ fill: var(--newtab-button-text);
height: 27px;
inset-inline-end: -13.5px;
opacity: 0;
@@ -1860,10 +2375,21 @@ main section {
transition-property: transform, opacity;
width: 27px;
}
-.card-outer .context-menu-button:is(:active, :focus) {
+.card-outer .context-menu-button:is(:active, :focus-visible, :hover) {
opacity: 1;
transform: scale(1);
}
+.card-outer .context-menu-button:is(:hover) {
+ background-color: var(--newtab-button-hover-background);
+}
+.card-outer .context-menu-button:is(:focus-visible) {
+ outline-color: var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-focus-background);
+ outline-width: 4px;
+}
+.card-outer .context-menu-button:is(:active) {
+ background-color: var(--newtab-button-active-background);
+}
.card-outer:is(:focus):not(.placeholder) {
border: 0;
outline: 0;
@@ -2470,6 +2996,15 @@ main section {
width: auto;
flex-grow: 1;
}
+.discoverystream-admin .weather-section {
+ margin-block-end: 24px;
+}
+.discoverystream-admin .weather-section form {
+ display: flex;
+}
+.discoverystream-admin .weather-section form label {
+ margin-inline-end: 12px;
+}
.pocket-logged-in-cta {
font-size: 13px;
@@ -3157,6 +3692,18 @@ main section {
.ds-highlights .section .section-list .card-outer a {
text-decoration: none;
}
+.ds-highlights .section .section-list .card-outer .context-menu-button {
+ background-color: var(--newtab-button-static-background);
+}
+.ds-highlights .section .section-list .card-outer .context-menu-button:hover {
+ background-color: var(--newtab-button-static-hover-background);
+}
+.ds-highlights .section .section-list .card-outer .context-menu-button:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+}
+.ds-highlights .section .section-list .card-outer .context-menu-button:focus-visible {
+ background-color: var(--newtab-button-static-focus-background);
+}
.ds-highlights .hide-for-narrow {
display: block;
}
@@ -3418,14 +3965,17 @@ main section {
.ds-card .context-menu-button,
.ds-signup .context-menu-button {
background-clip: padding-box;
- background-color: var(--newtab-background-color-secondary);
+ background-color: var(--newtab-button-background);
background-image: url("chrome://global/skin/icons/more.svg");
background-position: 55%;
- border: 1px solid var(--newtab-border-color);
+ border: 0;
+ outline: 1px solid var(--newtab-border-color);
+ outline-width: 0;
border-radius: 100%;
box-shadow: 0 2px rgba(12, 12, 13, 0.1);
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ color: var(--button-text-color);
+ fill: var(--newtab-button-text);
height: 27px;
inset-inline-end: -13.5px;
opacity: 0;
@@ -3436,11 +3986,25 @@ main section {
transition-property: transform, opacity;
width: 27px;
}
-.ds-card .context-menu-button:is(:active, :focus),
-.ds-signup .context-menu-button:is(:active, :focus) {
+.ds-card .context-menu-button:is(:active, :focus-visible, :hover),
+.ds-signup .context-menu-button:is(:active, :focus-visible, :hover) {
opacity: 1;
transform: scale(1);
}
+.ds-card .context-menu-button:is(:hover),
+.ds-signup .context-menu-button:is(:hover) {
+ background-color: var(--newtab-button-hover-background);
+}
+.ds-card .context-menu-button:is(:focus-visible),
+.ds-signup .context-menu-button:is(:focus-visible) {
+ outline-color: var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-focus-background);
+ outline-width: 4px;
+}
+.ds-card .context-menu-button:is(:active),
+.ds-signup .context-menu-button:is(:active) {
+ background-color: var(--newtab-button-active-background);
+}
.ds-card .context-menu,
.ds-signup .context-menu {
opacity: 0;
@@ -3515,11 +4079,6 @@ main section {
background-size: 15px;
fill: #FFF;
}
-.ds-card .card-stp-button-hover-background .context-menu-button {
- position: static;
- transition: none;
- border-radius: 3px;
-}
.ds-card .card-stp-button-hover-background .context-menu-position-container {
position: relative;
}
@@ -3542,6 +4101,9 @@ main section {
white-space: nowrap;
color: #FFF;
}
+.ds-card .card-stp-button-hover-background .card-stp-button:focus-visible {
+ outline: 2px solid var(--newtab-button-focus-border);
+}
.ds-card .card-stp-button-hover-background button,
.ds-card .card-stp-button-hover-background .context-menu {
pointer-events: auto;
@@ -3549,6 +4111,22 @@ main section {
.ds-card .card-stp-button-hover-background button {
cursor: pointer;
}
+.ds-card .context-menu-button {
+ position: static;
+ transition: none;
+ border-radius: 3px;
+ background-color: var(--newtab-button-static-background);
+}
+.ds-card .context-menu-button:hover {
+ background-color: var(--newtab-button-static-hover-background);
+}
+.ds-card .context-menu-button:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+}
+.ds-card .context-menu-button:focus-visible {
+ outline: 2px solid var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-static-focus-background);
+}
.ds-card.last-item .card-stp-button-hover-background .context-menu {
margin-inline-start: auto;
margin-inline-end: 18.5px;
diff --git a/browser/components/newtab/css/activity-stream-mac.css b/browser/components/newtab/css/activity-stream-mac.css
index 87b942818a..7b5ef57cb5 100644
--- a/browser/components/newtab/css/activity-stream-mac.css
+++ b/browser/components/newtab/css/activity-stream-mac.css
@@ -46,6 +46,16 @@ input {
--newtab-text-secondary-color: color-mix(in srgb, var(--newtab-text-primary-color) 70%, transparent);
--newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #000);
--newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #000);
+ --newtab-button-background: var(--button-background-color);
+ --newtab-button-focus-background: var(--newtab-button-background);
+ --newtab-button-focus-border: var(--focus-outline-color);
+ --newtab-button-hover-background: var(--button-background-color-hover);
+ --newtab-button-active-background: var(--button-background-color-active);
+ --newtab-button-text: var(--button-text-color);
+ --newtab-button-static-background: #F0F0F4;
+ --newtab-button-static-focus-background: var(--newtab-button-static-background);
+ --newtab-button-static-hover-background: #E0E0E6;
+ --newtab-button-static-active-background: #CFCFD8;
--newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent);
--newtab-element-secondary-hover-color: color-mix(in srgb, currentColor 12%, transparent);
--newtab-element-secondary-active-color: color-mix(in srgb, currentColor 25%, transparent);
@@ -83,6 +93,9 @@ input {
--newtab-primary-element-text-color: #2b2a33;
--newtab-wordmark-color: #fbfbfe;
--newtab-status-success: #7C6;
+ --newtab-button-static-background: #2B2A33;
+ --newtab-button-static-hover-background: #52525E;
+ --newtab-button-static-active-background: #5B5B66;
}
@media (prefers-contrast) {
@@ -96,7 +109,7 @@ input {
background-size: 16px;
-moz-context-properties: fill;
display: inline-block;
- color: var(--newtab-text-primary-color);
+ color: var(--icon-color);
fill: currentColor;
height: 16px;
vertical-align: middle;
@@ -146,6 +159,9 @@ input {
.icon.icon-info {
background-image: url("chrome://global/skin/icons/info.svg");
}
+.icon.icon-info-critical {
+ background-image: url("chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg");
+}
.icon.icon-help {
background-image: url("chrome://global/skin/icons/help.svg");
}
@@ -226,6 +242,9 @@ input {
.icon.icon-webextension {
background-image: url("chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg");
}
+.icon.icon-weather {
+ background-image: url("chrome://browser/skin/weather/sunny.svg");
+}
.icon.icon-highlights {
background-image: url("chrome://global/skin/icons/highlights.svg");
}
@@ -280,6 +299,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 +438,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 +528,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;
@@ -624,12 +682,17 @@ main section {
.top-site-outer:is(:hover) .context-menu-button {
opacity: 1;
}
+.top-site-outer.active .context-menu-button {
+ opacity: 1;
+ background-color: var(--newtab-button-active-background);
+}
.top-site-outer .context-menu-button {
background-image: url("chrome://global/skin/icons/more.svg");
+ background-color: var(--newtab-button-background);
border: 0;
border-radius: 4px;
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ fill: var(--newtab-button-text);
-moz-context-properties: fill;
height: 20px;
width: 20px;
@@ -639,11 +702,16 @@ main section {
top: -20px;
transition: opacity 200ms;
}
-.top-site-outer .context-menu-button:is(:active, :focus) {
- outline: 0;
+.top-site-outer .context-menu-button:hover {
+ background-color: var(--newtab-button-hover-background);
+}
+.top-site-outer .context-menu-button:hover:active {
+ background-color: var(--newtab-button-active-background);
+}
+.top-site-outer .context-menu-button:focus-visible {
+ background-color: var(--newtab-button-focus-background);
+ border-color: var(--newtab-button-focus-border);
opacity: 1;
- background-color: var(--newtab-element-hover-color);
- fill: var(--newtab-primary-action-background);
}
.top-site-outer .tile {
border-radius: 8px;
@@ -1640,7 +1708,8 @@ main section {
inset-block: 0;
inset-inline-end: 0;
z-index: 1001;
- padding: 16px;
+ padding-block: 0 var(--space-large);
+ padding-inline: var(--space-large);
overflow: auto;
transform: translateX(435px);
visibility: hidden;
@@ -1667,9 +1736,16 @@ main section {
.customize-menu.customize-animate-exit-active {
box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.2);
}
+.customize-menu .close-button-wrapper {
+ position: sticky;
+ top: 0;
+ padding-block-start: var(--space-large);
+ background-color: var(--newtab-background-color-secondary);
+ z-index: 1;
+}
.customize-menu .close-button {
margin-inline-start: auto;
- margin-bottom: 28px;
+ margin-inline-end: var(--space-large);
white-space: nowrap;
display: block;
background-color: var(--newtab-element-secondary-color);
@@ -1696,7 +1772,10 @@ main section {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, auto);
grid-row-gap: 32px;
- padding: 0 16px;
+ padding: var(--space-large);
+}
+.home-section .wallpapers-section h2 {
+ font-size: inherit;
}
.home-section .section moz-toggle {
margin-bottom: 10px;
@@ -1834,6 +1913,439 @@ 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;
+}
+
+:root {
+ --newtab-weather-content-font-size: 11px;
+ --newtab-weather-sponsor-font-size: 8px;
+}
+
+.weather {
+ font-size: var(--font-size-root);
+ position: absolute;
+ left: var(--space-xlarge);
+ top: var(--space-xlarge);
+ z-index: 1;
+}
+
+.weatherNotAvailable {
+ font-size: var(--newtab-weather-content-font-size);
+ color: var(--text-color-error);
+ display: flex;
+ align-items: center;
+}
+.weatherNotAvailable .icon {
+ fill: var(--icon-color-critical);
+ -moz-context-properties: fill;
+}
+
+.weatherCard {
+ margin-block-end: var(--space-xsmall);
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: stretch;
+ border-radius: var(--border-radius-medium);
+ overflow: hidden;
+}
+.weatherCard:hover ~ .weatherSponsorText, .weatherCard:focus-within ~ .weatherSponsorText {
+ visibility: visible;
+}
+.weatherCard:focus-within {
+ overflow: visible;
+}
+.weatherCard:hover {
+ box-shadow: var(--box-shadow-10);
+ background: var(--background-color-box);
+}
+.weatherCard a {
+ color: var(--text-color);
+}
+
+.weatherSponsorText {
+ visibility: hidden;
+ font-size: var(--newtab-weather-sponsor-font-size);
+ color: var(--text-color-deemphasized);
+}
+
+.weatherInfoLink, .weatherButtonContextMenuWrapper {
+ appearance: none;
+ background-color: var(--background-color-ghost);
+ border: 0;
+ padding: var(--space-small);
+ cursor: pointer;
+}
+.weatherInfoLink:hover, .weatherButtonContextMenuWrapper:hover {
+ background-color: var(--button-background-color-ghost-hover);
+}
+.weatherInfoLink:hover::after, .weatherButtonContextMenuWrapper:hover::after {
+ background-color: transparent;
+}
+.weatherInfoLink:hover:active, .weatherButtonContextMenuWrapper:hover:active {
+ background-color: var(--button-background-color-ghost-active);
+}
+.weatherInfoLink:focus-visible, .weatherButtonContextMenuWrapper:focus-visible {
+ outline: var(--focus-outline);
+}
+@media (prefers-color-scheme: dark) {
+ .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper {
+ background-color: rgba(35, 34, 43, 0.7);
+ }
+ .hasWallpaperDark .weatherInfoLink:hover, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover {
+ background-color: var(--newtab-button-static-hover-background);
+ }
+ .hasWallpaperDark .weatherInfoLink:hover:active, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+}
+@media (prefers-contrast) and (prefers-color-scheme: dark) {
+ .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper {
+ background-color: var(--background-color-box);
+ }
+}
+@media (prefers-color-scheme: light) {
+ .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper {
+ background-color: rgba(255, 255, 255, 0.7);
+ }
+ .hasWallpaperLight .weatherInfoLink:hover, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover {
+ background-color: var(--newtab-button-static-hover-background);
+ }
+ .hasWallpaperLight .weatherInfoLink:hover:active, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+}
+@media (prefers-contrast) and (prefers-color-scheme: light) {
+ .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper {
+ background-color: var(--background-color-box);
+ }
+}
+
+.weatherInfoLink {
+ display: flex;
+ gap: var(--space-medium);
+ padding: var(--space-small) var(--space-medium);
+ border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium);
+ text-decoration: none;
+ color: var(--text-color);
+ min-width: 130px;
+ max-width: 190px;
+ text-overflow: ellipsis;
+}
+@media (min-width: 610px) {
+ .weatherInfoLink {
+ min-width: unset;
+ }
+}
+.weatherInfoLink:hover ~ .weatherButtonContextMenuWrapper::after {
+ background-color: transparent;
+}
+.weatherInfoLink:focus-visible {
+ border-radius: var(--border-radius-medium);
+}
+.weatherInfoLink:focus-visible ~ .weatherButtonContextMenuWrapper::after {
+ background-color: transparent;
+}
+
+.weatherButtonContextMenuWrapper {
+ position: relative;
+ cursor: pointer;
+ border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0;
+ display: flex;
+ align-items: stretch;
+ width: 50px;
+ padding: 0;
+}
+.weatherButtonContextMenuWrapper::after {
+ content: "";
+ left: 0;
+ top: 10px;
+ height: calc(100% - 20px);
+ width: 1px;
+ background-color: var(--newtab-button-static-background);
+ display: block;
+ position: absolute;
+ z-index: 0;
+}
+@media (prefers-color-scheme: dark) {
+ .weatherButtonContextMenuWrapper::after {
+ background-color: var(--color-gray-70);
+ }
+}
+.weatherButtonContextMenuWrapper:hover::after {
+ background-color: transparent;
+}
+.weatherButtonContextMenuWrapper:focus-visible {
+ border-radius: var(--border-radius-medium);
+}
+.weatherButtonContextMenuWrapper:focus-visible::after {
+ background-color: transparent;
+}
+
+.weatherButtonContextMenu {
+ background-image: url("chrome://global/skin/icons/more.svg");
+ background-repeat: no-repeat;
+ background-size: var(--size-item-small) auto;
+ background-position: center;
+ background-color: transparent;
+ cursor: pointer;
+ fill: var(--icon-color);
+ -moz-context-properties: fill;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ appearance: none;
+ min-width: var(--size-item-large);
+}
+
+.weatherText {
+ height: min-content;
+}
+
+.weatherCityRow, .weatherForecastRow, .weatherDetailedSummaryRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-small);
+}
+
+.weatherForecastRow {
+ text-transform: uppercase;
+ font-weight: var(--font-weight-bold);
+}
+
+.weatherCityRow {
+ color: var(--text-color-deemphasized);
+}
+
+.weatherCity {
+ text-overflow: ellipsis;
+ font-size: var(--font-size-small);
+}
+
+.weatherCityRow + .weatherDetailedSummaryRow {
+ margin-block-start: var(--space-xsmall);
+}
+
+.weatherDetailedSummaryRow {
+ font-size: var(--newtab-weather-content-font-size);
+ gap: var(--space-large);
+}
+
+.weatherHighLowTemps {
+ display: flex;
+ gap: var(--space-xxsmall);
+ text-transform: uppercase;
+ word-spacing: var(--space-xxsmall);
+}
+
+.weatherTextSummary {
+ text-align: center;
+ max-width: 90px;
+}
+
+.weatherTemperature {
+ font-size: var(--font-size-large);
+}
+
+.weatherIconCol {
+ width: var(--size-item-large);
+ height: var(--size-item-large);
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ align-self: center;
+}
+
+.weatherIcon {
+ width: var(--size-item-large);
+ height: auto;
+ vertical-align: middle;
+}
+@media (prefers-contrast) {
+ .weatherIcon {
+ -moz-context-properties: fill, stroke;
+ fill: currentColor;
+ stroke: currentColor;
+ }
+}
+.weatherIcon.iconId1 {
+ content: url("chrome://browser/skin/weather/sunny.svg");
+}
+.weatherIcon.iconId2 {
+ content: url("chrome://browser/skin/weather/mostly-sunny.svg");
+}
+.weatherIcon:is(.iconId3, .iconId4, .iconId6) {
+ content: url("chrome://browser/skin/weather/partly-sunny.svg");
+}
+.weatherIcon.iconId5 {
+ content: url("chrome://browser/skin/weather/hazy-sunshine.svg");
+}
+.weatherIcon:is(.iconId7, .iconId8) {
+ content: url("chrome://browser/skin/weather/cloudy.svg");
+}
+.weatherIcon.iconId11 {
+ content: url("chrome://browser/skin/weather/fog.svg");
+}
+.weatherIcon.iconId12 {
+ content: url("chrome://browser/skin/weather/showers.svg");
+}
+.weatherIcon:is(.iconId13, .iconId14) {
+ content: url("chrome://browser/skin/weather/mostly-cloudy-with-showers.svg");
+}
+.weatherIcon.iconId15 {
+ content: url("chrome://browser/skin/weather/thunderstorms.svg");
+}
+.weatherIcon:is(.iconId16, .iconId17) {
+ content: url("chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg");
+}
+.weatherIcon.iconId18 {
+ content: url("chrome://browser/skin/weather/rain.svg");
+}
+.weatherIcon:is(.iconId19, .iconId20, .iconId25) {
+ content: url("chrome://browser/skin/weather/flurries.svg");
+}
+.weatherIcon.iconId21 {
+ content: url("chrome://browser/skin/weather/partly-sunny-with-flurries.svg");
+}
+.weatherIcon:is(.iconId22, .iconId23) {
+ content: url("chrome://browser/skin/weather/snow.svg");
+}
+.weatherIcon:is(.iconId24, .iconId31) {
+ content: url("chrome://browser/skin/weather/ice.svg");
+}
+.weatherIcon:is(.iconId26, .iconId29) {
+ content: url("chrome://browser/skin/weather/freezing-rain.svg");
+}
+.weatherIcon.iconId30 {
+ content: url("chrome://browser/skin/weather/hot.svg");
+}
+.weatherIcon.iconId32 {
+ content: url("chrome://browser/skin/weather/windy.svg");
+}
+.weatherIcon.iconId33 {
+ content: url("chrome://browser/skin/weather/night-clear.svg");
+}
+.weatherIcon:is(.iconId34, .iconId35, .iconId36, .iconId38) {
+ content: url("chrome://browser/skin/weather/night-mostly-clear.svg");
+}
+.weatherIcon.iconId37 {
+ content: url("chrome://browser/skin/weather/night-hazy-moonlight.svg");
+}
+.weatherIcon:is(.iconId39, .iconId40) {
+ content: url("chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg");
+ height: var(--size-item-large);
+}
+.weatherIcon:is(.iconId41, .iconId42) {
+ content: url("chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg");
+}
+.weatherIcon:is(.iconId43, .iconId44) {
+ content: url("chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg");
+}
+
/* stylelint-disable max-nesting-depth */
.card-outer {
background: var(--newtab-background-color-secondary);
@@ -1846,14 +2358,17 @@ main section {
}
.card-outer .context-menu-button {
background-clip: padding-box;
- background-color: var(--newtab-background-color-secondary);
+ background-color: var(--newtab-button-background);
background-image: url("chrome://global/skin/icons/more.svg");
background-position: 55%;
- border: 1px solid var(--newtab-border-color);
+ border: 0;
+ outline: 1px solid var(--newtab-border-color);
+ outline-width: 0;
border-radius: 100%;
box-shadow: 0 2px rgba(12, 12, 13, 0.1);
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ color: var(--button-text-color);
+ fill: var(--newtab-button-text);
height: 27px;
inset-inline-end: -13.5px;
opacity: 0;
@@ -1864,10 +2379,21 @@ main section {
transition-property: transform, opacity;
width: 27px;
}
-.card-outer .context-menu-button:is(:active, :focus) {
+.card-outer .context-menu-button:is(:active, :focus-visible, :hover) {
opacity: 1;
transform: scale(1);
}
+.card-outer .context-menu-button:is(:hover) {
+ background-color: var(--newtab-button-hover-background);
+}
+.card-outer .context-menu-button:is(:focus-visible) {
+ outline-color: var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-focus-background);
+ outline-width: 4px;
+}
+.card-outer .context-menu-button:is(:active) {
+ background-color: var(--newtab-button-active-background);
+}
.card-outer:is(:focus):not(.placeholder) {
border: 0;
outline: 0;
@@ -2474,6 +3000,15 @@ main section {
width: auto;
flex-grow: 1;
}
+.discoverystream-admin .weather-section {
+ margin-block-end: 24px;
+}
+.discoverystream-admin .weather-section form {
+ display: flex;
+}
+.discoverystream-admin .weather-section form label {
+ margin-inline-end: 12px;
+}
.pocket-logged-in-cta {
font-size: 13px;
@@ -3161,6 +3696,18 @@ main section {
.ds-highlights .section .section-list .card-outer a {
text-decoration: none;
}
+.ds-highlights .section .section-list .card-outer .context-menu-button {
+ background-color: var(--newtab-button-static-background);
+}
+.ds-highlights .section .section-list .card-outer .context-menu-button:hover {
+ background-color: var(--newtab-button-static-hover-background);
+}
+.ds-highlights .section .section-list .card-outer .context-menu-button:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+}
+.ds-highlights .section .section-list .card-outer .context-menu-button:focus-visible {
+ background-color: var(--newtab-button-static-focus-background);
+}
.ds-highlights .hide-for-narrow {
display: block;
}
@@ -3422,14 +3969,17 @@ main section {
.ds-card .context-menu-button,
.ds-signup .context-menu-button {
background-clip: padding-box;
- background-color: var(--newtab-background-color-secondary);
+ background-color: var(--newtab-button-background);
background-image: url("chrome://global/skin/icons/more.svg");
background-position: 55%;
- border: 1px solid var(--newtab-border-color);
+ border: 0;
+ outline: 1px solid var(--newtab-border-color);
+ outline-width: 0;
border-radius: 100%;
box-shadow: 0 2px rgba(12, 12, 13, 0.1);
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ color: var(--button-text-color);
+ fill: var(--newtab-button-text);
height: 27px;
inset-inline-end: -13.5px;
opacity: 0;
@@ -3440,11 +3990,25 @@ main section {
transition-property: transform, opacity;
width: 27px;
}
-.ds-card .context-menu-button:is(:active, :focus),
-.ds-signup .context-menu-button:is(:active, :focus) {
+.ds-card .context-menu-button:is(:active, :focus-visible, :hover),
+.ds-signup .context-menu-button:is(:active, :focus-visible, :hover) {
opacity: 1;
transform: scale(1);
}
+.ds-card .context-menu-button:is(:hover),
+.ds-signup .context-menu-button:is(:hover) {
+ background-color: var(--newtab-button-hover-background);
+}
+.ds-card .context-menu-button:is(:focus-visible),
+.ds-signup .context-menu-button:is(:focus-visible) {
+ outline-color: var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-focus-background);
+ outline-width: 4px;
+}
+.ds-card .context-menu-button:is(:active),
+.ds-signup .context-menu-button:is(:active) {
+ background-color: var(--newtab-button-active-background);
+}
.ds-card .context-menu,
.ds-signup .context-menu {
opacity: 0;
@@ -3519,11 +4083,6 @@ main section {
background-size: 15px;
fill: #FFF;
}
-.ds-card .card-stp-button-hover-background .context-menu-button {
- position: static;
- transition: none;
- border-radius: 3px;
-}
.ds-card .card-stp-button-hover-background .context-menu-position-container {
position: relative;
}
@@ -3546,6 +4105,9 @@ main section {
white-space: nowrap;
color: #FFF;
}
+.ds-card .card-stp-button-hover-background .card-stp-button:focus-visible {
+ outline: 2px solid var(--newtab-button-focus-border);
+}
.ds-card .card-stp-button-hover-background button,
.ds-card .card-stp-button-hover-background .context-menu {
pointer-events: auto;
@@ -3553,6 +4115,22 @@ main section {
.ds-card .card-stp-button-hover-background button {
cursor: pointer;
}
+.ds-card .context-menu-button {
+ position: static;
+ transition: none;
+ border-radius: 3px;
+ background-color: var(--newtab-button-static-background);
+}
+.ds-card .context-menu-button:hover {
+ background-color: var(--newtab-button-static-hover-background);
+}
+.ds-card .context-menu-button:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+}
+.ds-card .context-menu-button:focus-visible {
+ outline: 2px solid var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-static-focus-background);
+}
.ds-card.last-item .card-stp-button-hover-background .context-menu {
margin-inline-start: auto;
margin-inline-end: 18.5px;
diff --git a/browser/components/newtab/css/activity-stream-windows.css b/browser/components/newtab/css/activity-stream-windows.css
index 25370fdf19..96b27e6b5f 100644
--- a/browser/components/newtab/css/activity-stream-windows.css
+++ b/browser/components/newtab/css/activity-stream-windows.css
@@ -42,6 +42,16 @@ input {
--newtab-text-secondary-color: color-mix(in srgb, var(--newtab-text-primary-color) 70%, transparent);
--newtab-element-hover-color: color-mix(in srgb, var(--newtab-background-color) 90%, #000);
--newtab-element-active-color: color-mix(in srgb, var(--newtab-background-color) 80%, #000);
+ --newtab-button-background: var(--button-background-color);
+ --newtab-button-focus-background: var(--newtab-button-background);
+ --newtab-button-focus-border: var(--focus-outline-color);
+ --newtab-button-hover-background: var(--button-background-color-hover);
+ --newtab-button-active-background: var(--button-background-color-active);
+ --newtab-button-text: var(--button-text-color);
+ --newtab-button-static-background: #F0F0F4;
+ --newtab-button-static-focus-background: var(--newtab-button-static-background);
+ --newtab-button-static-hover-background: #E0E0E6;
+ --newtab-button-static-active-background: #CFCFD8;
--newtab-element-secondary-color: color-mix(in srgb, currentColor 5%, transparent);
--newtab-element-secondary-hover-color: color-mix(in srgb, currentColor 12%, transparent);
--newtab-element-secondary-active-color: color-mix(in srgb, currentColor 25%, transparent);
@@ -79,6 +89,9 @@ input {
--newtab-primary-element-text-color: #2b2a33;
--newtab-wordmark-color: #fbfbfe;
--newtab-status-success: #7C6;
+ --newtab-button-static-background: #2B2A33;
+ --newtab-button-static-hover-background: #52525E;
+ --newtab-button-static-active-background: #5B5B66;
}
@media (prefers-contrast) {
@@ -92,7 +105,7 @@ input {
background-size: 16px;
-moz-context-properties: fill;
display: inline-block;
- color: var(--newtab-text-primary-color);
+ color: var(--icon-color);
fill: currentColor;
height: 16px;
vertical-align: middle;
@@ -142,6 +155,9 @@ input {
.icon.icon-info {
background-image: url("chrome://global/skin/icons/info.svg");
}
+.icon.icon-info-critical {
+ background-image: url("chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg");
+}
.icon.icon-help {
background-image: url("chrome://global/skin/icons/help.svg");
}
@@ -222,6 +238,9 @@ input {
.icon.icon-webextension {
background-image: url("chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg");
}
+.icon.icon-weather {
+ background-image: url("chrome://browser/skin/weather/sunny.svg");
+}
.icon.icon-highlights {
background-image: url("chrome://global/skin/icons/highlights.svg");
}
@@ -276,6 +295,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 +434,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 +524,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;
@@ -620,12 +678,17 @@ main section {
.top-site-outer:is(:hover) .context-menu-button {
opacity: 1;
}
+.top-site-outer.active .context-menu-button {
+ opacity: 1;
+ background-color: var(--newtab-button-active-background);
+}
.top-site-outer .context-menu-button {
background-image: url("chrome://global/skin/icons/more.svg");
+ background-color: var(--newtab-button-background);
border: 0;
border-radius: 4px;
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ fill: var(--newtab-button-text);
-moz-context-properties: fill;
height: 20px;
width: 20px;
@@ -635,11 +698,16 @@ main section {
top: -20px;
transition: opacity 200ms;
}
-.top-site-outer .context-menu-button:is(:active, :focus) {
- outline: 0;
+.top-site-outer .context-menu-button:hover {
+ background-color: var(--newtab-button-hover-background);
+}
+.top-site-outer .context-menu-button:hover:active {
+ background-color: var(--newtab-button-active-background);
+}
+.top-site-outer .context-menu-button:focus-visible {
+ background-color: var(--newtab-button-focus-background);
+ border-color: var(--newtab-button-focus-border);
opacity: 1;
- background-color: var(--newtab-element-hover-color);
- fill: var(--newtab-primary-action-background);
}
.top-site-outer .tile {
border-radius: 8px;
@@ -1636,7 +1704,8 @@ main section {
inset-block: 0;
inset-inline-end: 0;
z-index: 1001;
- padding: 16px;
+ padding-block: 0 var(--space-large);
+ padding-inline: var(--space-large);
overflow: auto;
transform: translateX(435px);
visibility: hidden;
@@ -1663,9 +1732,16 @@ main section {
.customize-menu.customize-animate-exit-active {
box-shadow: 0 2px 14px 0 rgba(0, 0, 0, 0.2);
}
+.customize-menu .close-button-wrapper {
+ position: sticky;
+ top: 0;
+ padding-block-start: var(--space-large);
+ background-color: var(--newtab-background-color-secondary);
+ z-index: 1;
+}
.customize-menu .close-button {
margin-inline-start: auto;
- margin-bottom: 28px;
+ margin-inline-end: var(--space-large);
white-space: nowrap;
display: block;
background-color: var(--newtab-element-secondary-color);
@@ -1692,7 +1768,10 @@ main section {
grid-template-columns: 1fr;
grid-template-rows: repeat(4, auto);
grid-row-gap: 32px;
- padding: 0 16px;
+ padding: var(--space-large);
+}
+.home-section .wallpapers-section h2 {
+ font-size: inherit;
}
.home-section .section moz-toggle {
margin-bottom: 10px;
@@ -1830,6 +1909,439 @@ 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;
+}
+
+:root {
+ --newtab-weather-content-font-size: 11px;
+ --newtab-weather-sponsor-font-size: 8px;
+}
+
+.weather {
+ font-size: var(--font-size-root);
+ position: absolute;
+ left: var(--space-xlarge);
+ top: var(--space-xlarge);
+ z-index: 1;
+}
+
+.weatherNotAvailable {
+ font-size: var(--newtab-weather-content-font-size);
+ color: var(--text-color-error);
+ display: flex;
+ align-items: center;
+}
+.weatherNotAvailable .icon {
+ fill: var(--icon-color-critical);
+ -moz-context-properties: fill;
+}
+
+.weatherCard {
+ margin-block-end: var(--space-xsmall);
+ display: flex;
+ flex-wrap: nowrap;
+ align-items: stretch;
+ border-radius: var(--border-radius-medium);
+ overflow: hidden;
+}
+.weatherCard:hover ~ .weatherSponsorText, .weatherCard:focus-within ~ .weatherSponsorText {
+ visibility: visible;
+}
+.weatherCard:focus-within {
+ overflow: visible;
+}
+.weatherCard:hover {
+ box-shadow: var(--box-shadow-10);
+ background: var(--background-color-box);
+}
+.weatherCard a {
+ color: var(--text-color);
+}
+
+.weatherSponsorText {
+ visibility: hidden;
+ font-size: var(--newtab-weather-sponsor-font-size);
+ color: var(--text-color-deemphasized);
+}
+
+.weatherInfoLink, .weatherButtonContextMenuWrapper {
+ appearance: none;
+ background-color: var(--background-color-ghost);
+ border: 0;
+ padding: var(--space-small);
+ cursor: pointer;
+}
+.weatherInfoLink:hover, .weatherButtonContextMenuWrapper:hover {
+ background-color: var(--button-background-color-ghost-hover);
+}
+.weatherInfoLink:hover::after, .weatherButtonContextMenuWrapper:hover::after {
+ background-color: transparent;
+}
+.weatherInfoLink:hover:active, .weatherButtonContextMenuWrapper:hover:active {
+ background-color: var(--button-background-color-ghost-active);
+}
+.weatherInfoLink:focus-visible, .weatherButtonContextMenuWrapper:focus-visible {
+ outline: var(--focus-outline);
+}
+@media (prefers-color-scheme: dark) {
+ .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper {
+ background-color: rgba(35, 34, 43, 0.7);
+ }
+ .hasWallpaperDark .weatherInfoLink:hover, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover {
+ background-color: var(--newtab-button-static-hover-background);
+ }
+ .hasWallpaperDark .weatherInfoLink:hover:active, .hasWallpaperDark .weatherButtonContextMenuWrapper:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+}
+@media (prefers-contrast) and (prefers-color-scheme: dark) {
+ .hasWallpaperDark .weatherInfoLink, .hasWallpaperDark .weatherButtonContextMenuWrapper {
+ background-color: var(--background-color-box);
+ }
+}
+@media (prefers-color-scheme: light) {
+ .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper {
+ background-color: rgba(255, 255, 255, 0.7);
+ }
+ .hasWallpaperLight .weatherInfoLink:hover, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover {
+ background-color: var(--newtab-button-static-hover-background);
+ }
+ .hasWallpaperLight .weatherInfoLink:hover:active, .hasWallpaperLight .weatherButtonContextMenuWrapper:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+ }
+}
+@media (prefers-contrast) and (prefers-color-scheme: light) {
+ .hasWallpaperLight .weatherInfoLink, .hasWallpaperLight .weatherButtonContextMenuWrapper {
+ background-color: var(--background-color-box);
+ }
+}
+
+.weatherInfoLink {
+ display: flex;
+ gap: var(--space-medium);
+ padding: var(--space-small) var(--space-medium);
+ border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium);
+ text-decoration: none;
+ color: var(--text-color);
+ min-width: 130px;
+ max-width: 190px;
+ text-overflow: ellipsis;
+}
+@media (min-width: 610px) {
+ .weatherInfoLink {
+ min-width: unset;
+ }
+}
+.weatherInfoLink:hover ~ .weatherButtonContextMenuWrapper::after {
+ background-color: transparent;
+}
+.weatherInfoLink:focus-visible {
+ border-radius: var(--border-radius-medium);
+}
+.weatherInfoLink:focus-visible ~ .weatherButtonContextMenuWrapper::after {
+ background-color: transparent;
+}
+
+.weatherButtonContextMenuWrapper {
+ position: relative;
+ cursor: pointer;
+ border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0;
+ display: flex;
+ align-items: stretch;
+ width: 50px;
+ padding: 0;
+}
+.weatherButtonContextMenuWrapper::after {
+ content: "";
+ left: 0;
+ top: 10px;
+ height: calc(100% - 20px);
+ width: 1px;
+ background-color: var(--newtab-button-static-background);
+ display: block;
+ position: absolute;
+ z-index: 0;
+}
+@media (prefers-color-scheme: dark) {
+ .weatherButtonContextMenuWrapper::after {
+ background-color: var(--color-gray-70);
+ }
+}
+.weatherButtonContextMenuWrapper:hover::after {
+ background-color: transparent;
+}
+.weatherButtonContextMenuWrapper:focus-visible {
+ border-radius: var(--border-radius-medium);
+}
+.weatherButtonContextMenuWrapper:focus-visible::after {
+ background-color: transparent;
+}
+
+.weatherButtonContextMenu {
+ background-image: url("chrome://global/skin/icons/more.svg");
+ background-repeat: no-repeat;
+ background-size: var(--size-item-small) auto;
+ background-position: center;
+ background-color: transparent;
+ cursor: pointer;
+ fill: var(--icon-color);
+ -moz-context-properties: fill;
+ width: 100%;
+ height: 100%;
+ border: 0;
+ appearance: none;
+ min-width: var(--size-item-large);
+}
+
+.weatherText {
+ height: min-content;
+}
+
+.weatherCityRow, .weatherForecastRow, .weatherDetailedSummaryRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-small);
+}
+
+.weatherForecastRow {
+ text-transform: uppercase;
+ font-weight: var(--font-weight-bold);
+}
+
+.weatherCityRow {
+ color: var(--text-color-deemphasized);
+}
+
+.weatherCity {
+ text-overflow: ellipsis;
+ font-size: var(--font-size-small);
+}
+
+.weatherCityRow + .weatherDetailedSummaryRow {
+ margin-block-start: var(--space-xsmall);
+}
+
+.weatherDetailedSummaryRow {
+ font-size: var(--newtab-weather-content-font-size);
+ gap: var(--space-large);
+}
+
+.weatherHighLowTemps {
+ display: flex;
+ gap: var(--space-xxsmall);
+ text-transform: uppercase;
+ word-spacing: var(--space-xxsmall);
+}
+
+.weatherTextSummary {
+ text-align: center;
+ max-width: 90px;
+}
+
+.weatherTemperature {
+ font-size: var(--font-size-large);
+}
+
+.weatherIconCol {
+ width: var(--size-item-large);
+ height: var(--size-item-large);
+ aspect-ratio: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ align-self: center;
+}
+
+.weatherIcon {
+ width: var(--size-item-large);
+ height: auto;
+ vertical-align: middle;
+}
+@media (prefers-contrast) {
+ .weatherIcon {
+ -moz-context-properties: fill, stroke;
+ fill: currentColor;
+ stroke: currentColor;
+ }
+}
+.weatherIcon.iconId1 {
+ content: url("chrome://browser/skin/weather/sunny.svg");
+}
+.weatherIcon.iconId2 {
+ content: url("chrome://browser/skin/weather/mostly-sunny.svg");
+}
+.weatherIcon:is(.iconId3, .iconId4, .iconId6) {
+ content: url("chrome://browser/skin/weather/partly-sunny.svg");
+}
+.weatherIcon.iconId5 {
+ content: url("chrome://browser/skin/weather/hazy-sunshine.svg");
+}
+.weatherIcon:is(.iconId7, .iconId8) {
+ content: url("chrome://browser/skin/weather/cloudy.svg");
+}
+.weatherIcon.iconId11 {
+ content: url("chrome://browser/skin/weather/fog.svg");
+}
+.weatherIcon.iconId12 {
+ content: url("chrome://browser/skin/weather/showers.svg");
+}
+.weatherIcon:is(.iconId13, .iconId14) {
+ content: url("chrome://browser/skin/weather/mostly-cloudy-with-showers.svg");
+}
+.weatherIcon.iconId15 {
+ content: url("chrome://browser/skin/weather/thunderstorms.svg");
+}
+.weatherIcon:is(.iconId16, .iconId17) {
+ content: url("chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg");
+}
+.weatherIcon.iconId18 {
+ content: url("chrome://browser/skin/weather/rain.svg");
+}
+.weatherIcon:is(.iconId19, .iconId20, .iconId25) {
+ content: url("chrome://browser/skin/weather/flurries.svg");
+}
+.weatherIcon.iconId21 {
+ content: url("chrome://browser/skin/weather/partly-sunny-with-flurries.svg");
+}
+.weatherIcon:is(.iconId22, .iconId23) {
+ content: url("chrome://browser/skin/weather/snow.svg");
+}
+.weatherIcon:is(.iconId24, .iconId31) {
+ content: url("chrome://browser/skin/weather/ice.svg");
+}
+.weatherIcon:is(.iconId26, .iconId29) {
+ content: url("chrome://browser/skin/weather/freezing-rain.svg");
+}
+.weatherIcon.iconId30 {
+ content: url("chrome://browser/skin/weather/hot.svg");
+}
+.weatherIcon.iconId32 {
+ content: url("chrome://browser/skin/weather/windy.svg");
+}
+.weatherIcon.iconId33 {
+ content: url("chrome://browser/skin/weather/night-clear.svg");
+}
+.weatherIcon:is(.iconId34, .iconId35, .iconId36, .iconId38) {
+ content: url("chrome://browser/skin/weather/night-mostly-clear.svg");
+}
+.weatherIcon.iconId37 {
+ content: url("chrome://browser/skin/weather/night-hazy-moonlight.svg");
+}
+.weatherIcon:is(.iconId39, .iconId40) {
+ content: url("chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg");
+ height: var(--size-item-large);
+}
+.weatherIcon:is(.iconId41, .iconId42) {
+ content: url("chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg");
+}
+.weatherIcon:is(.iconId43, .iconId44) {
+ content: url("chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg");
+}
+
/* stylelint-disable max-nesting-depth */
.card-outer {
background: var(--newtab-background-color-secondary);
@@ -1842,14 +2354,17 @@ main section {
}
.card-outer .context-menu-button {
background-clip: padding-box;
- background-color: var(--newtab-background-color-secondary);
+ background-color: var(--newtab-button-background);
background-image: url("chrome://global/skin/icons/more.svg");
background-position: 55%;
- border: 1px solid var(--newtab-border-color);
+ border: 0;
+ outline: 1px solid var(--newtab-border-color);
+ outline-width: 0;
border-radius: 100%;
box-shadow: 0 2px rgba(12, 12, 13, 0.1);
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ color: var(--button-text-color);
+ fill: var(--newtab-button-text);
height: 27px;
inset-inline-end: -13.5px;
opacity: 0;
@@ -1860,10 +2375,21 @@ main section {
transition-property: transform, opacity;
width: 27px;
}
-.card-outer .context-menu-button:is(:active, :focus) {
+.card-outer .context-menu-button:is(:active, :focus-visible, :hover) {
opacity: 1;
transform: scale(1);
}
+.card-outer .context-menu-button:is(:hover) {
+ background-color: var(--newtab-button-hover-background);
+}
+.card-outer .context-menu-button:is(:focus-visible) {
+ outline-color: var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-focus-background);
+ outline-width: 4px;
+}
+.card-outer .context-menu-button:is(:active) {
+ background-color: var(--newtab-button-active-background);
+}
.card-outer:is(:focus):not(.placeholder) {
border: 0;
outline: 0;
@@ -2470,6 +2996,15 @@ main section {
width: auto;
flex-grow: 1;
}
+.discoverystream-admin .weather-section {
+ margin-block-end: 24px;
+}
+.discoverystream-admin .weather-section form {
+ display: flex;
+}
+.discoverystream-admin .weather-section form label {
+ margin-inline-end: 12px;
+}
.pocket-logged-in-cta {
font-size: 13px;
@@ -3157,6 +3692,18 @@ main section {
.ds-highlights .section .section-list .card-outer a {
text-decoration: none;
}
+.ds-highlights .section .section-list .card-outer .context-menu-button {
+ background-color: var(--newtab-button-static-background);
+}
+.ds-highlights .section .section-list .card-outer .context-menu-button:hover {
+ background-color: var(--newtab-button-static-hover-background);
+}
+.ds-highlights .section .section-list .card-outer .context-menu-button:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+}
+.ds-highlights .section .section-list .card-outer .context-menu-button:focus-visible {
+ background-color: var(--newtab-button-static-focus-background);
+}
.ds-highlights .hide-for-narrow {
display: block;
}
@@ -3418,14 +3965,17 @@ main section {
.ds-card .context-menu-button,
.ds-signup .context-menu-button {
background-clip: padding-box;
- background-color: var(--newtab-background-color-secondary);
+ background-color: var(--newtab-button-background);
background-image: url("chrome://global/skin/icons/more.svg");
background-position: 55%;
- border: 1px solid var(--newtab-border-color);
+ border: 0;
+ outline: 1px solid var(--newtab-border-color);
+ outline-width: 0;
border-radius: 100%;
box-shadow: 0 2px rgba(12, 12, 13, 0.1);
cursor: pointer;
- fill: var(--newtab-text-primary-color);
+ color: var(--button-text-color);
+ fill: var(--newtab-button-text);
height: 27px;
inset-inline-end: -13.5px;
opacity: 0;
@@ -3436,11 +3986,25 @@ main section {
transition-property: transform, opacity;
width: 27px;
}
-.ds-card .context-menu-button:is(:active, :focus),
-.ds-signup .context-menu-button:is(:active, :focus) {
+.ds-card .context-menu-button:is(:active, :focus-visible, :hover),
+.ds-signup .context-menu-button:is(:active, :focus-visible, :hover) {
opacity: 1;
transform: scale(1);
}
+.ds-card .context-menu-button:is(:hover),
+.ds-signup .context-menu-button:is(:hover) {
+ background-color: var(--newtab-button-hover-background);
+}
+.ds-card .context-menu-button:is(:focus-visible),
+.ds-signup .context-menu-button:is(:focus-visible) {
+ outline-color: var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-focus-background);
+ outline-width: 4px;
+}
+.ds-card .context-menu-button:is(:active),
+.ds-signup .context-menu-button:is(:active) {
+ background-color: var(--newtab-button-active-background);
+}
.ds-card .context-menu,
.ds-signup .context-menu {
opacity: 0;
@@ -3515,11 +4079,6 @@ main section {
background-size: 15px;
fill: #FFF;
}
-.ds-card .card-stp-button-hover-background .context-menu-button {
- position: static;
- transition: none;
- border-radius: 3px;
-}
.ds-card .card-stp-button-hover-background .context-menu-position-container {
position: relative;
}
@@ -3542,6 +4101,9 @@ main section {
white-space: nowrap;
color: #FFF;
}
+.ds-card .card-stp-button-hover-background .card-stp-button:focus-visible {
+ outline: 2px solid var(--newtab-button-focus-border);
+}
.ds-card .card-stp-button-hover-background button,
.ds-card .card-stp-button-hover-background .context-menu {
pointer-events: auto;
@@ -3549,6 +4111,22 @@ main section {
.ds-card .card-stp-button-hover-background button {
cursor: pointer;
}
+.ds-card .context-menu-button {
+ position: static;
+ transition: none;
+ border-radius: 3px;
+ background-color: var(--newtab-button-static-background);
+}
+.ds-card .context-menu-button:hover {
+ background-color: var(--newtab-button-static-hover-background);
+}
+.ds-card .context-menu-button:hover:active {
+ background-color: var(--newtab-button-static-active-background);
+}
+.ds-card .context-menu-button:focus-visible {
+ outline: 2px solid var(--newtab-button-focus-border);
+ background-color: var(--newtab-button-static-focus-background);
+}
.ds-card.last-item .card-stp-button-hover-background .context-menu {
margin-inline-start: auto;
margin-inline-end: 18.5px;
diff --git a/browser/components/newtab/data/content/activity-stream.bundle.js b/browser/components/newtab/data/content/activity-stream.bundle.js
index 8904ba87d1..45705fe58c 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,12 @@ for (const type of [
"UPDATE_PINNED_SEARCH_SHORTCUTS",
"UPDATE_SEARCH_SHORTCUTS",
"UPDATE_SECTION_PREFS",
+ "WALLPAPERS_SET",
+ "WALLPAPER_CLICK",
+ "WEATHER_IMPRESSION",
+ "WEATHER_LOAD_ERROR",
+ "WEATHER_OPEN_PROVIDER_URL",
+ "WEATHER_UPDATE",
"WEBEXT_CLICK",
"WEBEXT_DISMISS",
]) {
@@ -444,8 +452,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 +556,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("-");
@@ -669,8 +680,11 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent {
this.systemTick = this.systemTick.bind(this);
this.syncRemoteSettings = this.syncRemoteSettings.bind(this);
this.onStoryToggle = this.onStoryToggle.bind(this);
+ this.handleWeatherSubmit = this.handleWeatherSubmit.bind(this);
+ this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this);
this.state = {
- toggledStories: {}
+ toggledStories: {},
+ weatherQuery: ""
};
}
setConfigValue(name, value) {
@@ -713,6 +727,18 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent {
syncRemoteSettings() {
this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_SYNC_RS);
}
+ handleWeatherUpdate(e) {
+ this.setState({
+ weatherQuery: e.target.value || ""
+ });
+ }
+ handleWeatherSubmit(e) {
+ e.preventDefault();
+ const {
+ weatherQuery
+ } = this.state;
+ this.props.dispatch(actionCreators.SetPref("weather.query", weatherQuery));
+ }
renderComponent(width, component) {
return /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, /*#__PURE__*/external_React_default().createElement(Row, null, /*#__PURE__*/external_React_default().createElement("td", {
className: "min"
@@ -720,6 +746,38 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent {
className: "min"
}, "Width"), /*#__PURE__*/external_React_default().createElement("td", null, width)), component.feed && this.renderFeed(component.feed)));
}
+ renderWeatherData() {
+ const {
+ suggestions
+ } = this.props.state.Weather;
+ let weatherTable;
+ if (suggestions) {
+ weatherTable = /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weather-section"
+ }, /*#__PURE__*/external_React_default().createElement("form", {
+ onSubmit: this.handleWeatherSubmit
+ }, /*#__PURE__*/external_React_default().createElement("label", {
+ htmlFor: "weather-query"
+ }, "Weather query"), /*#__PURE__*/external_React_default().createElement("input", {
+ type: "text",
+ min: "3",
+ max: "10",
+ id: "weather-query",
+ onChange: this.handleWeatherUpdate,
+ value: this.weatherQuery
+ }), /*#__PURE__*/external_React_default().createElement("button", {
+ type: "submit"
+ }, "Submit")), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, suggestions.map(suggestion => /*#__PURE__*/external_React_default().createElement("tr", {
+ className: "message-item",
+ key: suggestion.city_name
+ }, /*#__PURE__*/external_React_default().createElement("td", {
+ className: "message-id"
+ }, /*#__PURE__*/external_React_default().createElement("span", null, suggestion.city_name, " ", /*#__PURE__*/external_React_default().createElement("br", null))), /*#__PURE__*/external_React_default().createElement("td", {
+ className: "message-summary"
+ }, /*#__PURE__*/external_React_default().createElement("pre", null, JSON.stringify(suggestion, null, 2))))))));
+ }
+ return weatherTable;
+ }
renderFeedData(url) {
const {
feeds
@@ -830,7 +888,7 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent {
state: {
Personalization: this.props.state.Personalization
}
- }), /*#__PURE__*/external_React_default().createElement("h3", null, "Spocs"), this.renderSpocs(), /*#__PURE__*/external_React_default().createElement("h3", null, "Feeds Data"), this.renderFeedsData());
+ }), /*#__PURE__*/external_React_default().createElement("h3", null, "Spocs"), this.renderSpocs(), /*#__PURE__*/external_React_default().createElement("h3", null, "Feeds Data"), this.renderFeedsData(), /*#__PURE__*/external_React_default().createElement("h3", null, "Weather Data"), this.renderWeatherData());
}
}
class DiscoveryStreamAdminInner extends (external_React_default()).PureComponent {
@@ -853,7 +911,8 @@ class DiscoveryStreamAdminInner extends (external_React_default()).PureComponent
}, "Click here"))), /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement(DiscoveryStreamAdminUI, {
state: {
DiscoveryStream: this.props.DiscoveryStream,
- Personalization: this.props.Personalization
+ Personalization: this.props.Personalization,
+ Weather: this.props.Weather
},
otherPrefs: this.props.Prefs.values,
dispatch: this.props.dispatch
@@ -882,9 +941,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 +953,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 {
@@ -923,7 +982,8 @@ const DiscoveryStreamAdmin = (0,external_ReactRedux_namespaceObject.connect)(sta
Sections: state.Sections,
DiscoveryStream: state.DiscoveryStream,
Personalization: state.Personalization,
- Prefs: state.Prefs
+ Prefs: state.Prefs,
+ Weather: state.Weather
}))(_DiscoveryStreamAdmin);
;// CONCATENATED MODULE: ./content-src/components/ConfirmDialog/ConfirmDialog.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public
@@ -1262,11 +1322,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 +1452,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 +1475,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 +1493,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 +1525,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 +1549,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 +1570,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 +1585,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 +1594,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 +1670,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 +1690,140 @@ 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 },
+ },
+ }),
+ 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(),
+ ChangeWeatherLocation: () => ({
+ id: "newtab-weather-menu-change-location",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.CHANGE_WEATHER_LOCATION,
+ data: { url: "https://mozilla.org" },
+ }),
+ }),
+ ChangeWeatherDisplaySimple: () => ({
+ id: "newtab-weather-menu-change-weather-display-simple",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.SET_PREF,
data: {
- index
- }
- }
+ name: "weather.display",
+ value: "simple",
+ },
+ }),
+ }),
+ ChangeWeatherDisplayDetailed: () => ({
+ id: "newtab-weather-menu-change-weather-display-detailed",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.SET_PREF,
+ data: {
+ name: "weather.display",
+ value: "detailed",
+ },
+ }),
+ }),
+ ChangeTempUnitFahrenheit: () => ({
+ id: "newtab-weather-menu-change-temperature-units-fahrenheit",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.SET_PREF,
+ data: {
+ name: "weather.temperatureUnits",
+ value: "f",
+ },
+ }),
+ }),
+ ChangeTempUnitCelsius: () => ({
+ id: "newtab-weather-menu-change-temperature-units-celsius",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.SET_PREF,
+ data: {
+ name: "weather.temperatureUnits",
+ value: "c",
+ },
+ }),
+ }),
+ HideWeather: () => ({
+ id: "newtab-weather-menu-hide-weather",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.SET_PREF,
+ data: {
+ name: "showWeather",
+ value: false,
+ },
+ }),
+ }),
+ OpenLearnMoreURL: site => ({
+ id: "newtab-weather-menu-learn-more",
+ action: actionCreators.OnlyToMain({
+ type: actionTypes.OPEN_LINK,
+ data: { url: site.url },
+ }),
}),
- 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 +2044,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 +2176,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 +2291,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 +2369,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 +2377,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 +2856,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 +2899,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 +3063,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, {
@@ -2933,11 +3085,11 @@ class _DSCard extends (external_React_default()).PureComponent {
ctaButtonVariant: ctaButtonVariant,
dispatch: this.props.dispatch,
spocMessageVariant: this.props.spocMessageVariant
- }), saveToPocketCard && /*#__PURE__*/external_React_default().createElement("div", {
+ }), /*#__PURE__*/external_React_default().createElement("div", {
className: "card-stp-button-hover-background"
}, /*#__PURE__*/external_React_default().createElement("div", {
className: "card-stp-button-position-wrapper"
- }, !this.props.flightId && stpButton(), /*#__PURE__*/external_React_default().createElement(DSLinkMenu, {
+ }, saveToPocketCard && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, !this.props.flightId && stpButton()), /*#__PURE__*/external_React_default().createElement(DSLinkMenu, {
id: this.props.id,
index: this.props.pos,
dispatch: this.props.dispatch,
@@ -2955,25 +3107,7 @@ class _DSCard extends (external_React_default()).PureComponent {
saveToPocketCard: saveToPocketCard,
pocket_button_enabled: pocketButtonEnabled,
isRecentSave: isRecentSave
- }))), !saveToPocketCard && /*#__PURE__*/external_React_default().createElement(DSLinkMenu, {
- id: this.props.id,
- index: this.props.pos,
- dispatch: this.props.dispatch,
- url: this.props.url,
- title: this.props.title,
- source: source,
- type: this.props.type,
- pocket_id: this.props.pocket_id,
- shim: this.props.shim,
- bookmarkGuid: this.props.bookmarkGuid,
- flightId: !this.props.is_collection ? this.props.flightId : undefined,
- showPrivacyInfo: !!this.props.flightId,
- hostRef: this.contextMenuButtonHostRef,
- onMenuUpdate: this.onMenuUpdate,
- onMenuShow: this.onMenuShow,
- pocket_button_enabled: pocketButtonEnabled,
- isRecentSave: isRecentSave
- }));
+ }))));
}
}
_DSCard.defaultProps = {
@@ -3273,7 +3407,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 +3683,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 +3699,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 +4159,7 @@ class _CollapsibleSection extends (external_React_default()).PureComponent {
}
}
_CollapsibleSection.defaultProps = {
- document: __webpack_require__.g.document || {
+ document: globalThis.document || {
addEventListener: () => {},
removeEventListener: () => {},
visibilityState: "hidden"
@@ -4111,7 +4247,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 +4579,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 +4598,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 +4612,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 +4966,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 +4982,7 @@ function _PerfService(options) {
this._perf = usablePerfObj;
}
}
+
_PerfService.prototype = {
/**
* Calls the underlying mark() method on the appropriate Window.performance
@@ -4851,6 +4995,7 @@ _PerfService.prototype = {
mark: function mark(str) {
this._perf.mark(str);
},
+
/**
* Calls the underlying getEntriesByName on the appropriate Window.performance
* object.
@@ -4859,9 +5004,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 +5026,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 +5037,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 +5056,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 +5631,15 @@ const INITIAL_STATE = {
// Hide the search box after handing off to AwesomeBar and user starts typing.
hide: false,
},
+ Wallpapers: {
+ wallpaperList: [],
+ },
+ Weather: {
+ // do we have the data from WeatherFeed yet?
+ initialized: false,
+ suggestions: [],
+ lastUpdated: null,
+ },
};
function App(prevState = INITIAL_STATE.App, action) {
@@ -6219,6 +6380,29 @@ 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;
+ }
+}
+
+function Weather(prevState = INITIAL_STATE.Weather, action) {
+ switch (action.type) {
+ case actionTypes.WEATHER_UPDATE:
+ return {
+ ...prevState,
+ suggestions: action.data.suggestions,
+ lastUpdated: action.data.date,
+ initialized: true,
+ };
+ default:
+ return prevState;
+ }
+}
+
const reducers = {
TopSites,
App,
@@ -6230,6 +6414,8 @@ const reducers = {
Personalization: Reducers_sys_Personalization,
DiscoveryStream,
Search,
+ Wallpapers,
+ Weather,
};
;// CONCATENATED MODULE: ./content-src/components/TopSites/TopSiteFormInput.jsx
@@ -6448,8 +6634,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 +7787,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 +7919,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 +8155,7 @@ class Section extends (external_React_default()).PureComponent {
}
}
Section.defaultProps = {
- document: __webpack_require__.g.document,
+ document: globalThis.document,
rows: [],
emptyState: {},
pref: {},
@@ -8188,20 +8374,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 +8389,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 +8421,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 +8580,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 +8625,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 +8772,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 +8807,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 +8965,104 @@ 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);
+ this.handleUserEvent({
+ selected_wallpaper: id,
+ hadPreviousWallpaper: !!this.props.activeWallpaper
+ });
+ // 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}`, "");
+ this.handleUserEvent({
+ selected_wallpaper: "none",
+ hadPreviousWallpaper: !!this.props.activeWallpaper
+ });
+ }
+
+ // Record user interaction when changing wallpaper and reseting wallpaper to default
+ handleUserEvent(data) {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.WALLPAPER_CLICK,
+ data
+ }));
+ }
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 +9071,7 @@ class BackgroundsSection extends (external_React_default()).PureComponent {
+
class ContentSection extends (external_React_default()).PureComponent {
constructor(props) {
super(props);
@@ -8760,7 +9092,7 @@ class ContentSection extends (external_React_default()).PureComponent {
}));
}
onPreferenceSelect(e) {
- // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS
+ // eventSource: TOP_SITES | TOP_STORIES | HIGHLIGHTS | WEATHER
const {
preference,
eventSource
@@ -8817,13 +9149,18 @@ class ContentSection extends (external_React_default()).PureComponent {
pocketRegion,
mayHaveSponsoredStories,
mayHaveRecentSaves,
+ mayHaveWeather,
openPreferences,
- spocMessageVariant
+ spocMessageVariant,
+ wallpapersEnabled,
+ activeWallpaper,
+ setPref
} = this.props;
const {
topSitesEnabled,
pocketEnabled,
highlightsEnabled,
+ weatherEnabled,
showSponsoredTopSitesEnabled,
showSponsoredPocketEnabled,
showRecentSavesEnabled,
@@ -8831,7 +9168,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", {
@@ -8952,6 +9296,19 @@ class ContentSection extends (external_React_default()).PureComponent {
"data-eventSource": "HIGHLIGHTS",
"data-l10n-id": "newtab-custom-recent-toggle",
"data-l10n-attrs": "label, description"
+ }))), mayHaveWeather && /*#__PURE__*/external_React_default().createElement("div", {
+ id: "weather-section",
+ className: "section"
+ }, /*#__PURE__*/external_React_default().createElement("label", {
+ className: "switch"
+ }, /*#__PURE__*/external_React_default().createElement("moz-toggle", {
+ id: "weather-toggle",
+ pressed: weatherEnabled || null,
+ onToggle: this.onPreferenceSelect,
+ "data-preference": "showWeather",
+ "data-eventSource": "WEATHER",
+ "data-l10n-id": "newtab-custom-weather-toggle",
+ "data-l10n-attrs": "label, description"
}))), pocketRegion && mayHaveSponsoredStories && spocMessageVariant === "variant-c" && /*#__PURE__*/external_React_default().createElement("div", {
className: "sponsored-content-info"
}, /*#__PURE__*/external_React_default().createElement("div", {
@@ -8979,7 +9336,6 @@ class ContentSection extends (external_React_default()).PureComponent {
-
class _CustomizeMenu extends (external_React_default()).PureComponent {
constructor(props) {
super(props);
@@ -9018,19 +9374,24 @@ class _CustomizeMenu extends (external_React_default()).PureComponent {
className: "customize-menu",
role: "dialog",
"data-l10n-id": "newtab-personalize-dialog-label"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "close-button-wrapper"
}, /*#__PURE__*/external_React_default().createElement("button", {
onClick: () => this.props.onClose(),
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,
mayHaveRecentSaves: this.props.DiscoveryStream.recentSavesEnabled,
+ mayHaveWeather: this.props.mayHaveWeather,
spocMessageVariant: this.props.spocMessageVariant,
dispatch: this.props.dispatch
}))));
@@ -9039,44 +9400,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,
@@ -9242,6 +9605,260 @@ class _Search extends (external_React_default()).PureComponent {
const Search_Search = (0,external_ReactRedux_namespaceObject.connect)(state => ({
Prefs: state.Prefs
}))(_Search);
+;// CONCATENATED MODULE: ./content-src/components/Weather/Weather.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/. */
+
+
+
+
+
+const Weather_VISIBLE = "visible";
+const Weather_VISIBILITY_CHANGE_EVENT = "visibilitychange";
+class _Weather extends (external_React_default()).PureComponent {
+ constructor(props) {
+ super(props);
+ this.state = {
+ contextMenuKeyboard: false,
+ showContextMenu: false,
+ url: "https://example.com",
+ impressionSeen: false,
+ errorSeen: false
+ };
+ this.setImpressionRef = element => {
+ this.impressionElement = element;
+ };
+ this.setErrorRef = element => {
+ this.errorElement = element;
+ };
+ this.onClick = this.onClick.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onUpdate = this.onUpdate.bind(this);
+ this.onProviderClick = this.onProviderClick.bind(this);
+ }
+ componentDidMount() {
+ const {
+ props
+ } = this;
+ if (!props.dispatch) {
+ return;
+ }
+ if (props.document.visibilityState === Weather_VISIBLE) {
+ // Setup the impression observer once the page is visible.
+ this.setImpressionObservers();
+ } else {
+ // We should only ever send the latest impression stats ping, so remove any
+ // older listeners.
+ if (this._onVisibilityChange) {
+ props.document.removeEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ this._onVisibilityChange = () => {
+ if (props.document.visibilityState === Weather_VISIBLE) {
+ // Setup the impression observer once the page is visible.
+ this.setImpressionObservers();
+ props.document.removeEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ };
+ props.document.addEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+ componentWillUnmount() {
+ // Remove observers on unmount
+ if (this.observer && this.impressionElement) {
+ this.observer.unobserve(this.impressionElement);
+ }
+ if (this.observer && this.errorElement) {
+ this.observer.unobserve(this.errorElement);
+ }
+ if (this._onVisibilityChange) {
+ this.props.document.removeEventListener(Weather_VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
+ }
+ }
+ setImpressionObservers() {
+ if (this.impressionElement) {
+ this.observer = new IntersectionObserver(this.onImpression.bind(this));
+ this.observer.observe(this.impressionElement);
+ }
+ if (this.errorElement) {
+ this.observer = new IntersectionObserver(this.onError.bind(this));
+ this.observer.observe(this.errorElement);
+ }
+ }
+ onImpression(entries) {
+ if (this.state) {
+ const entry = entries.find(e => e.isIntersecting);
+ if (entry) {
+ if (this.impressionElement) {
+ this.observer.unobserve(this.impressionElement);
+ }
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.WEATHER_IMPRESSION
+ }));
+
+ // Stop observing since element has been seen
+ this.setState({
+ impressionSeen: true
+ });
+ }
+ }
+ }
+ onError(entries) {
+ if (this.state) {
+ const entry = entries.find(e => e.isIntersecting);
+ if (entry) {
+ if (this.errorElement) {
+ this.observer.unobserve(this.errorElement);
+ }
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.WEATHER_LOAD_ERROR
+ }));
+
+ // Stop observing since element has been seen
+ this.setState({
+ errorSeen: true
+ });
+ }
+ }
+ }
+ openContextMenu(isKeyBoard) {
+ if (this.props.onUpdate) {
+ this.props.onUpdate(true);
+ }
+ this.setState({
+ showContextMenu: true,
+ contextMenuKeyboard: isKeyBoard
+ });
+ }
+ onClick(event) {
+ event.preventDefault();
+ this.openContextMenu(false, event);
+ }
+ onKeyDown(event) {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ this.openContextMenu(true, event);
+ }
+ }
+ onUpdate(showContextMenu) {
+ if (this.props.onUpdate) {
+ this.props.onUpdate(showContextMenu);
+ }
+ this.setState({
+ showContextMenu
+ });
+ }
+ onProviderClick() {
+ this.props.dispatch(actionCreators.OnlyToMain({
+ type: actionTypes.WEATHER_OPEN_PROVIDER_URL,
+ data: {
+ source: "WEATHER"
+ }
+ }));
+ }
+ render() {
+ // Check if weather should be rendered
+ const isWeatherEnabled = this.props.Prefs.values["system.showWeather"];
+ if (!isWeatherEnabled || !this.props.Weather.initialized) {
+ return false;
+ }
+ const {
+ showContextMenu
+ } = this.state;
+ const WEATHER_SUGGESTION = this.props.Weather.suggestions?.[0];
+ const {
+ className,
+ index,
+ dispatch,
+ eventSource,
+ shouldSendImpressionStats
+ } = this.props;
+ const {
+ props
+ } = this;
+ const isContextMenuOpen = this.state.activeCard === index;
+ const outerClassName = ["weather", className, isContextMenuOpen && "active", props.placeholder && "placeholder"].filter(v => v).join(" ");
+ const showDetailedView = this.props.Prefs.values["weather.display"] === "detailed";
+
+ // Note: The temperature units/display options will become secondary menu items
+ const WEATHER_SOURCE_CONTEXT_MENU_OPTIONS = [...(this.props.Prefs.values["weather.locationSearchEnabled"] ? ["ChangeWeatherLocation"] : []), ...(this.props.Prefs.values["weather.temperatureUnits"] === "f" ? ["ChangeTempUnitCelsius"] : ["ChangeTempUnitFahrenheit"]), ...(this.props.Prefs.values["weather.display"] === "simple" ? ["ChangeWeatherDisplayDetailed"] : ["ChangeWeatherDisplaySimple"]), "HideWeather", "OpenLearnMoreURL"];
+
+ // Only return the widget if we have data. Otherwise, show error state
+ if (WEATHER_SUGGESTION) {
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ ref: this.setImpressionRef,
+ className: outerClassName
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weatherCard"
+ }, /*#__PURE__*/external_React_default().createElement("a", {
+ "data-l10n-id": "newtab-weather-see-forecast",
+ "data-l10n-args": "{\"provider\": \"AccuWeather\"}",
+ href: WEATHER_SUGGESTION.forecast.url,
+ className: "weatherInfoLink",
+ onClick: this.onProviderClick
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weatherIconCol"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: `weatherIcon iconId${WEATHER_SUGGESTION.current_conditions.icon_id}`
+ })), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weatherText"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weatherForecastRow"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "weatherTemperature"
+ }, WEATHER_SUGGESTION.current_conditions.temperature[this.props.Prefs.values["weather.temperatureUnits"]], "\xB0", this.props.Prefs.values["weather.temperatureUnits"])), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weatherCityRow"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "weatherCity"
+ }, WEATHER_SUGGESTION.city_name)), showDetailedView ? /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weatherDetailedSummaryRow"
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weatherHighLowTemps"
+ }, /*#__PURE__*/external_React_default().createElement("span", null, WEATHER_SUGGESTION.forecast.high[this.props.Prefs.values["weather.temperatureUnits"]], "\xB0", this.props.Prefs.values["weather.temperatureUnits"]), /*#__PURE__*/external_React_default().createElement("span", null, "\u2022"), /*#__PURE__*/external_React_default().createElement("span", null, WEATHER_SUGGESTION.forecast.low[this.props.Prefs.values["weather.temperatureUnits"]], "\xB0", this.props.Prefs.values["weather.temperatureUnits"])), /*#__PURE__*/external_React_default().createElement("span", {
+ className: "weatherTextSummary"
+ }, WEATHER_SUGGESTION.current_conditions.summary)) : null)), /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weatherButtonContextMenuWrapper"
+ }, /*#__PURE__*/external_React_default().createElement("button", {
+ "aria-haspopup": "true",
+ onKeyDown: this.onKeyDown,
+ onClick: this.onClick,
+ "data-l10n-id": "newtab-menu-section-tooltip",
+ className: "weatherButtonContextMenu"
+ }, showContextMenu ? /*#__PURE__*/external_React_default().createElement(LinkMenu, {
+ dispatch: dispatch,
+ index: index,
+ source: eventSource,
+ onUpdate: this.onUpdate,
+ options: WEATHER_SOURCE_CONTEXT_MENU_OPTIONS,
+ site: {
+ url: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page"
+ },
+ link: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page",
+ shouldSendImpressionStats: shouldSendImpressionStats
+ }) : null))), /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": "newtab-weather-sponsored",
+ "data-l10n-args": "{\"provider\": \"AccuWeather\"}",
+ className: "weatherSponsorText"
+ }));
+ }
+ return /*#__PURE__*/external_React_default().createElement("div", {
+ ref: this.setErrorRef,
+ className: outerClassName
+ }, /*#__PURE__*/external_React_default().createElement("div", {
+ className: "weatherNotAvailable"
+ }, /*#__PURE__*/external_React_default().createElement("span", {
+ className: "icon icon-small-spacer icon-info-critical"
+ }), " ", /*#__PURE__*/external_React_default().createElement("span", {
+ "data-l10n-id": "newtab-weather-error-not-available"
+ })));
+ }
+}
+const Weather_Weather = (0,external_ReactRedux_namespaceObject.connect)(state => ({
+ Weather: state.Weather,
+ Prefs: state.Prefs,
+ IntersectionObserver: globalThis.IntersectionObserver,
+ document: globalThis.document
+}))(_Weather);
;// CONCATENATED MODULE: ./content-src/components/Base/Base.jsx
function Base_extends() { Base_extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return Base_extends.apply(this, arguments); }
/* This Source Code Form is subject to the terms of the Mozilla Public
@@ -9258,6 +9875,9 @@ 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 +9926,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 +9957,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 +10054,61 @@ 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.string,
+ "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 || ""})`);
+
+ // Add helper class to body if user has a wallpaper selected
+ if (lightWallpaper) {
+ __webpack_require__.g.document?.body.classList.add("hasWallpaperLight");
+ }
+ if (darkWallpaper) {
+ __webpack_require__.g.document?.body.classList.add("hasWallpaperDark");
+ }
+ }
+ }
render() {
const {
props
@@ -9408,6 +10121,9 @@ 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 weatherEnabled = prefs.showWeather;
const {
pocketConfig
} = prefs;
@@ -9427,23 +10143,31 @@ class BaseContent extends (external_React_default()).PureComponent {
showSponsoredTopSitesEnabled: prefs.showSponsoredTopSites,
showSponsoredPocketEnabled: prefs.showSponsored,
showRecentSavesEnabled: prefs.showRecentSaves,
- topSitesRowsCount: prefs.topSitesRows
+ topSitesRowsCount: prefs.topSitesRows,
+ weatherEnabled: prefs.showWeather
};
const pocketRegion = prefs["feeds.system.topstories"];
const mayHaveSponsoredStories = prefs["system.showSponsored"];
+ const mayHaveWeather = prefs["system.showWeather"];
const {
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,
+ mayHaveWeather: mayHaveWeather,
spocMessageVariant: spocMessageVariant,
showing: customizeMenuVisible
}), /*#__PURE__*/external_React_default().createElement("div", {
@@ -9460,31 +10184,39 @@ 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()), /*#__PURE__*/external_React_default().createElement("aside", null, weatherEnabled && /*#__PURE__*/external_React_default().createElement(ErrorBoundary, null, /*#__PURE__*/external_React_default().createElement(Weather_Weather, null)))));
}
}
+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,
+ Weather: state.Weather
}))(_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 +10234,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 +10248,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 +10274,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 +10292,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 +10320,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 +10339,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 +10349,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 +10374,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 +10400,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/glyph-info-critical-16.svg b/browser/components/newtab/data/content/assets/glyph-info-critical-16.svg
new file mode 100644
index 0000000000..4fed88fb21
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/glyph-info-critical-16.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 http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none">
+ <path d="M7.625 16C6.64009 16 5.66482 15.806 4.75487 15.4291C3.84493 15.0522 3.01814 14.4997 2.3217 13.8033C1.62526 13.1069 1.07281 12.2801 0.695904 11.3701C0.318993 10.4602 0.125 9.48491 0.125 8.5C0.125 7.51509 0.318993 6.53982 0.695904 5.62987C1.07281 4.71993 1.62526 3.89314 2.3217 3.1967C3.01814 2.50026 3.84493 1.94781 4.75487 1.5709C5.66482 1.19399 6.64009 1 7.625 1C9.61412 1 11.5218 1.79018 12.9283 3.1967C14.3348 4.60322 15.125 6.51088 15.125 8.5C15.125 10.4891 14.3348 12.3968 12.9283 13.8033C11.5218 15.2098 9.61412 16 7.625 16ZM8.25 5.125C8.25 4.95924 8.18415 4.80027 8.06694 4.68306C7.94973 4.56585 7.79076 4.5 7.625 4.5C7.45924 4.5 7.30027 4.56585 7.18306 4.68306C7.06585 4.80027 7 4.95924 7 5.125V9.563C7 9.72876 7.06585 9.88773 7.18306 10.0049C7.30027 10.1222 7.45924 10.188 7.625 10.188C7.79076 10.188 7.94973 10.1222 8.06694 10.0049C8.18415 9.88773 8.25 9.72876 8.25 9.563V5.125ZM8.25 11.5L8 11.25H7.25L7 11.5V12.25L7.25 12.5H8L8.25 12.25V11.5Z" fill="context-fill"/>
+</svg> \ No newline at end of file
diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif
new file mode 100644
index 0000000000..5b77286079
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/dark-beach.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-color.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-color.avif
new file mode 100644
index 0000000000..a4fc8e2341
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/dark-color.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif
new file mode 100644
index 0000000000..ed22325f00
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/dark-landscape.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif
new file mode 100644
index 0000000000..a704809a12
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/dark-mountain.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif
new file mode 100644
index 0000000000..decfff669b
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/dark-panda.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif b/browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif
new file mode 100644
index 0000000000..51eea392ca
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/dark-sky.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-beach.avif b/browser/components/newtab/data/content/assets/wallpapers/light-beach.avif
new file mode 100644
index 0000000000..b5f7b2ae67
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/light-beach.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-color.avif b/browser/components/newtab/data/content/assets/wallpapers/light-color.avif
new file mode 100644
index 0000000000..3366b7aec6
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/light-color.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif b/browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif
new file mode 100644
index 0000000000..1776091825
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/light-landscape.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif b/browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif
new file mode 100644
index 0000000000..5983c942fc
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/light-mountain.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-panda.avif b/browser/components/newtab/data/content/assets/wallpapers/light-panda.avif
new file mode 100644
index 0000000000..d20f405e45
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/light-panda.avif
Binary files differ
diff --git a/browser/components/newtab/data/content/assets/wallpapers/light-sky.avif b/browser/components/newtab/data/content/assets/wallpapers/light-sky.avif
new file mode 100644
index 0000000000..f152f00e06
--- /dev/null
+++ b/browser/components/newtab/data/content/assets/wallpapers/light-sky.avif
Binary files differ
diff --git a/browser/components/newtab/karma.mc.config.js b/browser/components/newtab/karma.mc.config.js
index fa3ac14587..89887918e0 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,24 @@ 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,
+ },
+ /**
+ * Weather.jsx is tested via an xpcshell test
+ */
+ "content-src/components/Weather/*.jsx": {
+ statements: 0,
+ lines: 0,
+ functions: 0,
+ branches: 0,
+ },
"content-src/components/DiscoveryStreamAdmin/*.jsx": {
statements: 0,
lines: 0,
@@ -211,7 +238,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 +287,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..7d13214361 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";
@@ -50,6 +50,26 @@ const PREFS_BEFORE_SECTIONS = () => [
rowsPref: "topSitesRows",
eventSource: "TOP_SITES",
},
+ {
+ id: "weather",
+ icon: "chrome://browser/skin/weather/sunny.svg",
+ pref: {
+ feed: "showWeather",
+ titleString: "home-prefs-weather-header",
+ descString: "home-prefs-weather-description",
+ learnMore: {
+ link: {
+ href: "https://support.mozilla.org/kb/customize-items-on-firefox-new-tab-page",
+ id: "home-prefs-weather-learn-more-link",
+ },
+ },
+ },
+ eventSource: "WEATHER",
+ shouldHidePref: !Services.prefs.getBoolPref(
+ "browser.newtabpage.activity-stream.system.showWeather",
+ false
+ ),
+ },
];
export class AboutPreferences {
@@ -74,7 +94,7 @@ export class AboutPreferences {
break;
// This is used to open the web extension settings page for an extension
case at.OPEN_WEBEXT_SETTINGS:
- action._target.browser.ownerGlobal.BrowserOpenAddonsMgr(
+ action._target.browser.ownerGlobal.BrowserAddonUI.openAddonsMgr(
`addons://detail/${encodeURIComponent(action.data)}`
);
break;
@@ -213,15 +233,13 @@ export class AboutPreferences {
linkPref(checkbox, name, "bool");
- // Specially add a link for stories
- if (id === "topstories") {
- const sponsoredHbox = createAppend("hbox", sectionVbox);
- sponsoredHbox.setAttribute("align", "center");
- sponsoredHbox.appendChild(checkbox);
+ // Specially add a link for Recommended stories and Weather
+ if (id === "topstories" || id === "weather") {
+ const hboxWithLink = createAppend("hbox", sectionVbox);
+ hboxWithLink.appendChild(checkbox);
checkbox.classList.add("tail-with-learn-more");
- const link = createAppend("label", sponsoredHbox, { is: "text-link" });
- link.classList.add("learn-sponsored");
+ const link = createAppend("label", hboxWithLink, { is: "text-link" });
link.setAttribute("href", sectionData.pref.learnMore.link.href);
document.l10n.setAttributes(link, sectionData.pref.learnMore.link.id);
}
diff --git a/browser/components/newtab/lib/ActivityStream.sys.mjs b/browser/components/newtab/lib/ActivityStream.sys.mjs
index f46e8aadf0..430707ab5b 100644
--- a/browser/components/newtab/lib/ActivityStream.sys.mjs
+++ b/browser/components/newtab/lib/ActivityStream.sys.mjs
@@ -36,6 +36,8 @@ 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",
+ WeatherFeed: "resource://activity-stream/lib/WeatherFeed.sys.mjs",
});
// NB: Eagerly load modules that will be loaded/constructed/initialized in the
@@ -43,7 +45,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";
@@ -56,6 +58,16 @@ function showSpocs({ geo }) {
return spocsGeo.includes(geo);
}
+function showWeather({ geo }) {
+ const weatherGeoString =
+ lazy.NimbusFeatures.pocketNewtab.getVariable("regionWeatherConfig") || "";
+ const weatherGeo = weatherGeoString
+ .split(",")
+ .map(s => s.trim())
+ .filter(item => item);
+ return weatherGeo.includes(geo);
+}
+
// Configure default Activity Stream prefs with a plain `value` or a `getValue`
// that computes a value. A `value_local_dev` is used for development defaults.
export const PREFS_CONFIG = new Map([
@@ -131,6 +143,50 @@ export const PREFS_CONFIG = new Map([
},
],
[
+ "system.showWeather",
+ {
+ title: "system.showWeather",
+ // pref is dynamic
+ getValue: showWeather,
+ },
+ ],
+ [
+ "showWeather",
+ {
+ title: "showWeather",
+ value: true,
+ },
+ ],
+ [
+ "weather.query",
+ {
+ title: "weather.query",
+ value: "",
+ },
+ ],
+ [
+ "weather.locationSearchEnabled",
+ {
+ title: "Enable the option to search for a specific city",
+ value: false,
+ },
+ ],
+ [
+ "weather.temperatureUnits",
+ {
+ title: "Switch the temperature between Celsius and Fahrenheit",
+ value: "f",
+ },
+ ],
+ [
+ "weather.display",
+ {
+ title:
+ "Toggle the weather widget to include a text summary of the current conditions",
+ value: "simple",
+ },
+ ],
+ [
"pocketCta",
{
title: "Pocket cta and button for logged out users.",
@@ -233,6 +289,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 +601,18 @@ 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,
+ },
+ {
+ name: "weatherfeed",
+ factory: () => new lazy.WeatherFeed(),
+ title: "Handles fetching and caching weather data",
+ 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/ActivityStreamStorage.sys.mjs b/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs
index 1e128ec3f2..22a1dea2a9 100644
--- a/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs
+++ b/browser/components/newtab/lib/ActivityStreamStorage.sys.mjs
@@ -38,6 +38,7 @@ export class ActivityStreamStorage {
return {
get: this._get.bind(this, storeName),
getAll: this._getAll.bind(this, storeName),
+ getAllKeys: this._getAllKeys.bind(this, storeName),
set: this._set.bind(this, storeName),
};
}
@@ -61,6 +62,12 @@ export class ActivityStreamStorage {
);
}
+ _getAllKeys(storeName) {
+ return this._requestWrapper(async () =>
+ (await this._getStore(storeName)).getAllKeys()
+ );
+ }
+
_set(storeName, key, value) {
return this._requestWrapper(async () =>
(await this._getStore(storeName)).put(value, key)
@@ -68,7 +75,7 @@ export class ActivityStreamStorage {
}
_openDatabase() {
- return lazy.IndexedDB.open(this.dbName, { version: this.dbVersion }, db => {
+ return lazy.IndexedDB.open(this.dbName, this.dbVersion, db => {
// If provided with array of objectStore names we need to create all the
// individual stores
this.storeNames.forEach(store => {
diff --git a/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs b/browser/components/newtab/lib/DiscoveryStreamFeed.sys.mjs
index ee08462503..e1f5dff6ce 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
@@ -564,10 +564,17 @@ export class DiscoveryStreamFeed {
}
generateFeedUrl(isBff) {
+ // check for experiment parameters
+ const hasParameters = lazy.NimbusFeatures.pocketNewtab.getVariable(
+ "pocketFeedParameters"
+ );
+
if (isBff) {
- return `https://${lazy.NimbusFeatures.saveToPocket.getVariable(
- "bffApi"
- )}/desktop/v1/recommendations?locale=$locale&region=$region&count=30`;
+ return `https://${Services.prefs.getStringPref(
+ "extensions.pocket.bffApi"
+ )}/desktop/v1/recommendations?locale=$locale&region=$region&count=30${
+ hasParameters || ""
+ }`;
}
return FEED_URL;
}
@@ -986,8 +993,9 @@ export class DiscoveryStreamFeed {
});
if (spocsResponse) {
+ const fetchTimestamp = Date.now();
spocsState = {
- lastUpdated: Date.now(),
+ lastUpdated: fetchTimestamp,
spocs: {
...spocsResponse,
},
@@ -1050,8 +1058,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 +1222,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 +1363,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 +1797,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 +1823,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..3646ebc73a 100644
--- a/browser/components/newtab/lib/DownloadsManager.sys.mjs
+++ b/browser/components/newtab/lib/DownloadsManager.sys.mjs
@@ -2,11 +2,12 @@
* 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 = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
DownloadsViewUI: "resource:///modules/DownloadsViewUI.sys.mjs",
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
@@ -166,10 +167,8 @@ export class DownloadsManager {
);
});
break;
- case at.OPEN_DOWNLOAD_FILE:
- const win = action._target.browser.ownerGlobal;
- const openWhere =
- action.data.event && win.whereToOpenLink(action.data.event);
+ case at.OPEN_DOWNLOAD_FILE: {
+ const openWhere = lazy.BrowserUtils.whereToOpenLink(action.data.event);
doDownloadAction(download => {
lazy.DownloadsCommon.openDownload(download, {
// Replace "current" or unknown value with "tab" as the default behavior
@@ -180,6 +179,7 @@ export class DownloadsManager {
});
});
break;
+ }
case at.UNINIT:
this.uninit();
break;
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..78e6873b3d 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";
@@ -24,6 +24,7 @@ const { AboutNewTab } = ChromeUtils.importESModule(
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
@@ -274,7 +275,7 @@ export class PlacesFeed {
const win = action._target.browser.ownerGlobal;
win.openTrustedLinkIn(
urlToOpen,
- where || win.whereToOpenLink(event),
+ where || lazy.BrowserUtils.whereToOpenLink(event),
params
);
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..2643337674 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";
@@ -114,6 +114,7 @@ const NEWTAB_PING_PREFS = {
"feeds.section.topstories": Glean.pocket.enabled,
showSponsored: Glean.pocket.sponsoredStoriesEnabled,
topSitesRows: Glean.topsites.rows,
+ showWeather: Glean.newtab.weatherEnabled,
};
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
@@ -454,8 +455,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 +509,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 +715,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 +748,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 +771,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;
@@ -907,9 +933,87 @@ export class TelemetryFeed {
case at.BLOCK_URL:
this.handleBlockUrl(action);
break;
+ case at.WALLPAPER_CLICK:
+ this.handleWallpaperUserEvent(action);
+ break;
+ case at.SET_PREF:
+ this.handleSetPref(action);
+ break;
+ case at.WEATHER_IMPRESSION:
+ this.handleWeatherUserEvent(action);
+ break;
+ case at.WEATHER_LOAD_ERROR:
+ this.handleWeatherUserEvent(action);
+ break;
+ case at.WEATHER_OPEN_PROVIDER_URL:
+ this.handleWeatherUserEvent(action);
+ break;
+ }
+ }
+
+ handleSetPref(action) {
+ const prefName = action.data.name;
+
+ // TODO: Migrate this event to handleWeatherUserEvent()
+ if (prefName === "weather.display") {
+ const session = this.sessions.get(au.getPortIdOfSender(action));
+
+ if (!session) {
+ return;
+ }
+
+ Glean.newtab.weatherChangeDisplay.record({
+ newtab_visit_id: session.session_id,
+ weather_display_mode: action.data.value,
+ });
+ }
+ }
+
+ handleWeatherUserEvent(action) {
+ const session = this.sessions.get(au.getPortIdOfSender(action));
+
+ if (!session) {
+ return;
+ }
+
+ // Weather specific telemtry events can be added and parsed here.
+ switch (action.type) {
+ case "WEATHER_IMPRESSION":
+ Glean.newtab.weatherImpression.record({
+ newtab_visit_id: session.session_id,
+ });
+ break;
+ case "WEATHER_LOAD_ERROR":
+ Glean.newtab.weatherLoadError.record({
+ newtab_visit_id: session.session_id,
+ });
+ break;
+ case "WEATHER_OPEN_PROVIDER_URL":
+ Glean.newtab.weatherOpenProviderUrl.record({
+ newtab_visit_id: session.session_id,
+ });
+ break;
+ default:
+ break;
}
}
+ handleWallpaperUserEvent(action) {
+ const session = this.sessions.get(au.getPortIdOfSender(action));
+
+ if (!session) {
+ return;
+ }
+ const { data } = action;
+ const { selected_wallpaper, hadPreviousWallpaper } = data;
+ // if either of the wallpaper prefs are truthy, they had a previous wallpaper
+ Glean.newtab.wallpaperClick.record({
+ newtab_visit_id: session.session_id,
+ selected_wallpaper,
+ hadPreviousWallpaper,
+ });
+ }
+
handleBlockUrl(action) {
const session = this.sessions.get(au.getPortIdOfSender(action));
// TODO: Do we want to not send this unless there's a newtab_visit_id?
@@ -976,6 +1080,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..7ab85466c6 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,
@@ -73,7 +73,7 @@ const ROWS_PREF = "topSitesRows";
const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
// The default total number of sponsored top sites to fetch from Contile
// and Pocket.
-const MAX_NUM_SPONSORED = 2;
+const MAX_NUM_SPONSORED = 3;
// Nimbus variable for the total number of sponsored top sites including
// both Contile and Pocket sources.
// The default will be `MAX_NUM_SPONSORED` if this variable is unspecified.
@@ -112,7 +112,7 @@ const NIMBUS_VARIABLE_CONTILE_POSITIONS = "contileTopsitesPositions";
const CONTILE_ENDPOINT_PREF = "browser.topsites.contile.endpoint";
const CONTILE_UPDATE_INTERVAL = 15 * 60 * 1000; // 15 minutes
// The maximum number of sponsored top sites to fetch from Contile.
-const CONTILE_MAX_NUM_SPONSORED = 2;
+const CONTILE_MAX_NUM_SPONSORED = 3;
const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles";
const CONTILE_CACHE_VALID_FOR_PREF = "browser.topsites.contile.cacheValidFor";
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/lib/WeatherFeed.sys.mjs b/browser/components/newtab/lib/WeatherFeed.sys.mjs
new file mode 100644
index 0000000000..16aa8196af
--- /dev/null
+++ b/browser/components/newtab/lib/WeatherFeed.sys.mjs
@@ -0,0 +1,208 @@
+/* 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, {
+ MerinoClient: "resource:///modules/MerinoClient.sys.mjs",
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ PersistentCache: "resource://activity-stream/lib/PersistentCache.sys.mjs",
+});
+
+import {
+ actionTypes as at,
+ actionCreators as ac,
+} from "resource://activity-stream/common/Actions.mjs";
+
+const CACHE_KEY = "weather_feed";
+const WEATHER_UPDATE_TIME = 10 * 60 * 1000; // 10 minutes
+const MERINO_PROVIDER = "accuweather";
+
+const PREF_WEATHER_QUERY = "weather.query";
+const PREF_SHOW_WEATHER = "showWeather";
+const PREF_SYSTEM_SHOW_WEATHER = "system.showWeather";
+
+/**
+ * A feature that periodically fetches weather suggestions from Merino for HNT.
+ */
+export class WeatherFeed {
+ constructor() {
+ this.loaded = false;
+ this.merino = null;
+ this.suggestions = [];
+ this.lastUpdated = null;
+ this.fetchTimer = null;
+ this.fetchIntervalMs = 30 * 60 * 1000; // 30 minutes
+ this.timeoutMS = 5000;
+ this.lastFetchTimeMs = 0;
+ this.fetchDelayAfterComingOnlineMs = 3000; // 3s
+ this.cache = this.PersistentCache(CACHE_KEY, true);
+ }
+
+ async resetCache() {
+ if (this.cache) {
+ await this.cache.set("weather", {});
+ }
+ }
+
+ async resetWeather() {
+ await this.resetCache();
+ this.suggestions = [];
+ this.lastUpdated = null;
+ }
+
+ isEnabled() {
+ return (
+ this.store.getState().Prefs.values[PREF_SHOW_WEATHER] &&
+ this.store.getState().Prefs.values[PREF_SYSTEM_SHOW_WEATHER]
+ );
+ }
+
+ async init() {
+ await this.loadWeather(true /* isStartup */);
+ }
+
+ stopFetching() {
+ if (!this.merino) {
+ return;
+ }
+
+ lazy.clearTimeout(this.fetchTimer);
+ this.merino = null;
+ this.suggestions = null;
+ this.fetchTimer = 0;
+ }
+
+ /**
+ * This thin wrapper around the fetch call makes it easier for us to write
+ * automated tests that simulate responses.
+ */
+ async fetchHelper() {
+ this.restartFetchTimer();
+ const weatherQuery = this.store.getState().Prefs.values[PREF_WEATHER_QUERY];
+ let suggestions = [];
+ try {
+ suggestions = await this.merino.fetch({
+ query: weatherQuery || "",
+ providers: [MERINO_PROVIDER],
+ timeoutMs: 5000,
+ });
+ } catch (error) {
+ // We don't need to do anything with this right now.
+ }
+
+ // results from the API or empty array if null
+ this.suggestions = suggestions ?? [];
+ }
+
+ async fetch(isStartup) {
+ // Keep a handle on the `MerinoClient` instance that exists at the start of
+ // this fetch. If fetching stops or this `Weather` instance is uninitialized
+ // during the fetch, `#merino` will be nulled, and the fetch should stop. We
+ // can compare `merino` to `this.merino` to tell when this occurs.
+ this.merino = await this.MerinoClient("HNT_WEATHER_FEED");
+ await this.fetchHelper();
+
+ if (this.suggestions.length) {
+ this.lastUpdated = this.Date().now();
+ await this.cache.set("weather", {
+ suggestions: this.suggestions,
+ lastUpdated: this.lastUpdated,
+ });
+ }
+
+ this.update(isStartup);
+ }
+
+ async loadWeather(isStartup = false) {
+ const cachedData = (await this.cache.get()) || {};
+ const { weather } = cachedData;
+
+ // If we have nothing in cache, or cache has expired, we can make a fresh fetch.
+ if (
+ !weather?.lastUpdated ||
+ !(this.Date().now() - weather.lastUpdated < WEATHER_UPDATE_TIME)
+ ) {
+ await this.fetch(isStartup);
+ } else if (!this.lastUpdated) {
+ this.suggestions = weather.suggestions;
+ this.lastUpdated = weather.lastUpdated;
+ this.update(isStartup);
+ }
+ }
+
+ update(isStartup) {
+ this.store.dispatch(
+ ac.BroadcastToContent({
+ type: at.WEATHER_UPDATE,
+ data: {
+ suggestions: this.suggestions,
+ lastUpdated: this.lastUpdated,
+ },
+ meta: {
+ isStartup,
+ },
+ })
+ );
+ }
+
+ restartFetchTimer(ms = this.fetchIntervalMs) {
+ lazy.clearTimeout(this.fetchTimer);
+ this.fetchTimer = lazy.setTimeout(() => {
+ this.fetch();
+ }, ms);
+ }
+
+ async onPrefChangedAction(action) {
+ switch (action.data.name) {
+ case PREF_WEATHER_QUERY:
+ await this.loadWeather();
+ break;
+ case PREF_SHOW_WEATHER:
+ case PREF_SYSTEM_SHOW_WEATHER:
+ if (this.isEnabled() && action.data.value) {
+ await this.loadWeather();
+ } else {
+ await this.resetWeather();
+ }
+ break;
+ }
+ }
+
+ async onAction(action) {
+ switch (action.type) {
+ case at.INIT:
+ if (this.isEnabled()) {
+ await this.init();
+ }
+ break;
+ case at.UNINIT:
+ await this.resetWeather();
+ break;
+ case at.DISCOVERY_STREAM_DEV_SYSTEM_TICK:
+ case at.SYSTEM_TICK:
+ if (this.isEnabled()) {
+ await this.loadWeather();
+ }
+ break;
+ case at.PREF_CHANGED:
+ await this.onPrefChangedAction(action);
+ break;
+ }
+ }
+}
+
+/**
+ * Creating a thin wrapper around MerinoClient, PersistentCache, and Date.
+ * This makes it easier for us to write automated tests that simulate responses.
+ */
+WeatherFeed.prototype.MerinoClient = (...args) => {
+ return new lazy.MerinoClient(...args);
+};
+WeatherFeed.prototype.PersistentCache = (...args) => {
+ return new lazy.PersistentCache(...args);
+};
+WeatherFeed.prototype.Date = () => {
+ return Date;
+};
diff --git a/browser/components/newtab/metrics.yaml b/browser/components/newtab/metrics.yaml
index bd74e609ad..4a75cd65eb 100644
--- a/browser/components/newtab/metrics.yaml
+++ b/browser/components/newtab/metrics.yaml
@@ -233,6 +233,132 @@ newtab:
send_in_pings:
- newtab
+ wallpaper_click:
+ type: event
+ description: >
+ Recorded when a user clicks on a wallpaper option
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1896004
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1896004
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - nbarrett@mozilla.com
+ expires: never
+ extra_keys:
+ selected_wallpaper:
+ description: >
+ Which wallpaper has been selected by the user
+ Will be the title of a Wallpaper or 'none' for users
+ that reset the background to default
+ type: string
+ had_previous_wallpaper:
+ description: >
+ Wheather or not user had a previously set wallpaper
+ type: boolean
+ newtab_visit_id: *newtab_visit_id
+ send_in_pings:
+ - newtab
+
+
+ weather_change_display:
+ type: event
+ description: >
+ Recorded when a user changes the weather display.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - mcrawford@mozilla.com
+ expires: never
+ extra_keys:
+ weather_display_mode: &weather_display_mode
+ description: >
+ Which display mode is selected.
+ type: boolean
+ newtab_visit_id: *newtab_visit_id
+ send_in_pings:
+ - newtab
+
+ weather_enabled:
+ lifetime: application
+ type: boolean
+ description: >
+ Whether the weather widget is enabled on the newtab.
+ Corresponds to the value of the
+ `browser.newtabpage.activity-stream.showWeather` pref.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1899340
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1899340
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - mcrawford@mozilla.com
+ - sdowne@mozilla.com
+ expires: never
+ send_in_pings:
+ - newtab
+
+ weather_open_provider_url:
+ type: event
+ description: >
+ Recorded when a user opens a link to the Weather provider website.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - mcrawford@mozilla.com
+ expires: never
+ extra_keys:
+ newtab_visit_id: *newtab_visit_id
+ send_in_pings:
+ - newtab
+
+ weather_impression:
+ type: event
+ description: >
+ Recorded when the weather widget is viewed
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1898275
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - mcrawford@mozilla.com
+ expires: never
+ extra_keys:
+ newtab_visit_id: *newtab_visit_id
+ send_in_pings:
+ - newtab
+
+ weather_load_error:
+ type: event
+ description: >
+ Recorded when the weather widget is not available
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1898275
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1895797
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - mcrawford@mozilla.com
+ expires: never
+ extra_keys:
+ newtab_visit_id: *newtab_visit_id
+ send_in_pings:
+ - newtab
+
+
newtab.search:
enabled:
lifetime: application
@@ -271,9 +397,10 @@ newtab.handoff_preference:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1864496
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1864496
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1892148
data_sensitivity:
- interaction
- expires: 128
+ expires: 131
notification_emails:
- fx-search-telemetry@mozilla.com
@@ -817,6 +944,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 +1187,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/abouthomecache/browser_experiments_api_control.js b/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js
index a94f1fe055..0b49b3eb69 100644
--- a/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js
+++ b/browser/components/newtab/test/browser/abouthomecache/browser_experiments_api_control.js
@@ -28,7 +28,9 @@ add_task(async function test_experiments_api_control() {
});
Assert.ok(
- !NimbusFeatures.abouthomecache.getVariable("enabled"),
+ !Services.prefs.getBoolPref(
+ "browser.startup.homepage.abouthome_cache.enabled"
+ ),
"NimbusFeatures should tell us that the about:home startup cache " +
"is disabled"
);
@@ -51,7 +53,9 @@ add_task(async function test_experiments_api_control() {
});
Assert.ok(
- NimbusFeatures.abouthomecache.getVariable("enabled"),
+ Services.prefs.getBoolPref(
+ "browser.startup.homepage.abouthome_cache.enabled"
+ ),
"NimbusFeatures should tell us that the about:home startup cache " +
"is enabled"
);
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_customize_menu_content.js b/browser/components/newtab/test/browser/browser_customize_menu_content.js
index ba83f1ff0a..fab032937a 100644
--- a/browser/components/newtab/test/browser/browser_customize_menu_content.js
+++ b/browser/components/newtab/test/browser/browser_customize_menu_content.js
@@ -1,5 +1,15 @@
"use strict";
+const { WeatherFeed } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/WeatherFeed.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ MerinoTestUtils: "resource://testing-common/MerinoTestUtils.sys.mjs",
+});
+
+const { WEATHER_SUGGESTION } = MerinoTestUtils;
+
test_newtab({
async before({ pushPrefs }) {
await pushPrefs(
@@ -118,6 +128,79 @@ test_newtab({
});
test_newtab({
+ async before({ pushPrefs }) {
+ sinon.stub(WeatherFeed.prototype, "MerinoClient").returns({
+ fetch: () => [WEATHER_SUGGESTION],
+ });
+ await pushPrefs(
+ ["browser.newtabpage.activity-stream.system.showWeather", true],
+ ["browser.newtabpage.activity-stream.showWeather", false]
+ );
+ },
+ test: async function test_render_customizeMenuWeather() {
+ // Weather Widget Fecthing
+ function getWeatherWidget() {
+ return content.document.querySelector(`.weather`);
+ }
+
+ function promiseWeatherShown() {
+ return ContentTaskUtils.waitForMutationCondition(
+ content.document.querySelector("aside"),
+ { childList: true, subtree: true },
+ () => getWeatherWidget()
+ );
+ }
+
+ const WEATHER_PREF = "browser.newtabpage.activity-stream.showWeather";
+
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.querySelector(".personalize-button"),
+ "Wait for prefs button to load on the newtab page"
+ );
+
+ let customizeButton = content.document.querySelector(".personalize-button");
+ customizeButton.click();
+
+ let defaultPos = "matrix(1, 0, 0, 1, 0, 0)";
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.getComputedStyle(
+ content.document.querySelector(".customize-menu")
+ ).transform === defaultPos,
+ "Customize Menu should be visible on screen"
+ );
+
+ // Test that clicking the weather toggle will make the
+ // weather widget appear on the newtab page.
+ //
+ // We waive XRay wrappers because we want to call the click()
+ // method defined on the toggle from this context.
+ let weatherSwitch = Cu.waiveXrays(
+ content.document.querySelector("#weather-section moz-toggle")
+ );
+ Assert.ok(
+ !Services.prefs.getBoolPref(WEATHER_PREF),
+ "Weather pref is turned off"
+ );
+ Assert.ok(!getWeatherWidget(), "Weather widget is not rendered");
+
+ let sectionShownPromise = promiseWeatherShown();
+ weatherSwitch.click();
+ await sectionShownPromise;
+
+ Assert.ok(getWeatherWidget(), "Weather widget is rendered");
+ },
+ async after() {
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.showWeather"
+ );
+ Services.prefs.clearUserPref(
+ "browser.newtabpage.activity-stream.system.showWeather"
+ );
+ },
+});
+
+test_newtab({
test: async function test_open_close_customizeMenu() {
const EventUtils = ContentTaskUtils.getEventUtils(content);
await ContentTaskUtils.waitForCondition(
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..6186ca71fe 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";
@@ -10,6 +10,7 @@ const DEFAULT_PROPS = {
},
mayHaveSponsoredTopSites: true,
mayHaveSponsoredStories: true,
+ mayHaveWeather: true,
pocketRegion: true,
dispatch: sinon.stub(),
setPref: sinon.stub(),
@@ -68,5 +69,9 @@ describe("ContentSection", () => {
wrapper.find("#highlights-toggle").prop("data-eventSource"),
"HIGHLIGHTS"
);
+ assert.equal(
+ wrapper.find("#weather-toggle").prop("data-eventSource"),
+ "WEATHER"
+ );
});
});
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..006e83e663 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,
@@ -70,6 +67,9 @@ describe("DiscoveryStreamAdmin", () => {
otherPrefs={{}}
state={{
DiscoveryStream: state,
+ Weather: {
+ suggestions: [],
+ },
}}
/>
);
@@ -93,7 +93,12 @@ describe("DiscoveryStreamAdmin", () => {
wrapper = shallow(
<DiscoveryStreamAdminUI
otherPrefs={{}}
- state={{ DiscoveryStream: state }}
+ state={{
+ DiscoveryStream: state,
+ Weather: {
+ suggestions: [],
+ },
+ }}
/>
);
wrapper.instance().onStoryToggle({ id: 12345 });
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..796f805444 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>", () => {
@@ -71,7 +70,9 @@ describe("<DSCard>", () => {
});
it("should render DSLinkMenu", () => {
- assert.equal(wrapper.children().at(3).type(), DSLinkMenu);
+ // Note: <DSLinkMenu> component moved from a direct child element of `.ds-card`. See Bug 1893936
+ const default_link_menu = wrapper.find(DSLinkMenu);
+ assert.ok(default_link_menu.exists());
});
it("should start with no .active class", () => {
@@ -174,6 +175,8 @@ describe("<DSCard>", () => {
card_type: "organic",
recommendation_id: undefined,
tile_id: "fooidx",
+ fetchTimestamp: DEFAULT_PROPS.fetchTimestamp,
+ firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp,
},
})
);
@@ -212,6 +215,8 @@ describe("<DSCard>", () => {
card_type: "spoc",
recommendation_id: undefined,
tile_id: "fooidx",
+ fetchTimestamp: DEFAULT_PROPS.fetchTimestamp,
+ firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp,
},
})
);
@@ -258,6 +263,8 @@ describe("<DSCard>", () => {
recommendation_id: undefined,
tile_id: "fooidx",
shim: "click shim",
+ fetchTimestamp: DEFAULT_PROPS.fetchTimestamp,
+ firstVisibleTimestamp: DEFAULT_PROPS.firstVisibleTimestamp,
},
})
);
@@ -370,7 +377,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 +403,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..6555a1b77e 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", () => {
@@ -57,17 +54,21 @@ describe("AboutPreferences Feed", () => {
instance.onAction(action);
assert.calledOnce(action._target.browser.ownerGlobal.openPreferences);
});
- it("should call .BrowserOpenAddonsMgr with the extension id on OPEN_WEBEXT_SETTINGS", () => {
+ it("should call .BrowserAddonUI.openAddonsMgr with the extension id on OPEN_WEBEXT_SETTINGS", () => {
const action = {
type: at.OPEN_WEBEXT_SETTINGS,
data: "foo",
_target: {
- browser: { ownerGlobal: { BrowserOpenAddonsMgr: sinon.spy() } },
+ browser: {
+ ownerGlobal: {
+ BrowserAddonUI: { openAddonsMgr: sinon.spy() },
+ },
+ },
},
};
instance.onAction(action);
assert.calledWith(
- action._target.browser.ownerGlobal.BrowserOpenAddonsMgr,
+ action._target.browser.ownerGlobal.BrowserAddonUI.openAddonsMgr,
"addons://detail/foo"
);
});
@@ -125,8 +126,9 @@ describe("AboutPreferences Feed", () => {
const [, structure] = stub.firstCall.args;
assert.equal(structure[0].id, "search");
assert.equal(structure[1].id, "topsites");
- assert.equal(structure[2].id, "topstories");
- assert.isEmpty(structure[2].rowsPref);
+ assert.equal(structure[2].id, "weather");
+ assert.equal(structure[3].id, "topstories");
+ assert.isEmpty(structure[3].rowsPref);
});
});
describe("#renderPreferences", () => {
diff --git a/browser/components/newtab/test/unit/lib/ActivityStream.test.js b/browser/components/newtab/test/unit/lib/ActivityStream.test.js
index b9deba1069..ed00eb8202 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";
@@ -311,6 +311,38 @@ describe("ActivityStream", () => {
);
});
});
+ describe("discoverystream.region-weather-config", () => {
+ let getVariableStub;
+ beforeEach(() => {
+ getVariableStub = sandbox.stub(
+ global.NimbusFeatures.pocketNewtab,
+ "getVariable"
+ );
+ sandbox.stub(global.Region, "home").get(() => "CA");
+ });
+ it("should turn off weather system pref if no region weather config is set and no geo is set", () => {
+ getVariableStub.withArgs("regionWeatherConfig").returns("");
+ sandbox.stub(global.Region, "home").get(() => "");
+
+ as._updateDynamicPrefs();
+
+ assert.isFalse(PREFS_CONFIG.get("system.showWeather").value);
+ });
+ it("should turn on weather system pref based on region weather config pref", () => {
+ getVariableStub.withArgs("regionWeatherConfig").returns("CA");
+
+ as._updateDynamicPrefs();
+
+ assert.isTrue(PREFS_CONFIG.get("system.showWeather").value);
+ });
+ it("should turn off weather system pref if no region weather config is set", () => {
+ getVariableStub.withArgs("regionWeatherConfig").returns("");
+
+ as._updateDynamicPrefs();
+
+ assert.isFalse(PREFS_CONFIG.get("system.showWeather").value);
+ });
+ });
describe("_updateDynamicPrefs topstories default value", () => {
let getVariableStub;
let getBoolPrefStub;
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/ActivityStreamStorage.test.js b/browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js
index 0b8baef762..fd56a3e185 100644
--- a/browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js
+++ b/browser/components/newtab/test/unit/lib/ActivityStreamStorage.test.js
@@ -47,6 +47,7 @@ describe("ActivityStreamStorage", () => {
beforeEach(() => {
storeStub = {
getAll: sandbox.stub().resolves(),
+ getAllKeys: sandbox.stub().resolves(),
get: sandbox.stub().resolves(),
put: sandbox.stub().resolves(),
};
@@ -75,6 +76,14 @@ describe("ActivityStreamStorage", () => {
assert.calledOnce(storeStub.getAll);
assert.deepEqual(result, ["bar"]);
});
+ it("should return the correct value for getAllKeys", async () => {
+ storeStub.getAllKeys.resolves(["key1", "key2", "key3"]);
+
+ const result = await testStorage.getAllKeys();
+
+ assert.calledOnce(storeStub.getAllKeys);
+ assert.deepEqual(result, ["key1", "key2", "key3"]);
+ });
it("should query the correct object store", async () => {
await testStorage.get();
diff --git a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js
index 92e10facb3..72fc6bd0b8 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 () => {
@@ -3469,6 +3467,22 @@ describe("DiscoveryStreamFeed", () => {
"https://bffApi/desktop/v1/recommendations?locale=$locale&region=$region&count=30"
);
});
+ it("should update the new feed url with pocketFeedParameters", async () => {
+ globals.set("NimbusFeatures", {
+ pocketNewtab: {
+ getVariable: sandbox.stub(),
+ },
+ });
+ global.NimbusFeatures.pocketNewtab.getVariable
+ .withArgs("pocketFeedParameters")
+ .returns("&enableRankingByRegion=1");
+ await feed.loadLayout(feed.store.dispatch);
+ const { layout } = feed.store.getState().DiscoveryStream;
+ assert.equal(
+ layout[0].components[2].feed.url,
+ "https://bffApi/desktop/v1/recommendations?locale=$locale&region=$region&count=30&enableRankingByRegion=1"
+ );
+ });
it("should fetch proper data from getComponentFeed", async () => {
const fakeCache = {};
sandbox.stub(feed.cache, "get").returns(Promise.resolve(fakeCache));
diff --git a/browser/components/newtab/test/unit/lib/DownloadsManager.test.js b/browser/components/newtab/test/unit/lib/DownloadsManager.test.js
index ac262baf90..23ee9ffa34 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";
@@ -29,6 +29,10 @@ describe("Downloads Manager", () => {
showDownloadedFile: sinon.stub(),
});
+ globals.set("BrowserUtils", {
+ whereToOpenLink: sinon.stub().returns("current"),
+ });
+
downloadsManager = new DownloadsManager();
downloadsManager.init({ dispatch() {} });
downloadsManager.onDownloadAdded({
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..d6f7079d77 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, {
@@ -424,7 +424,7 @@ add_task(async function test_onAction_OPEN_LINK() {
data: { url: "https://foo.com" },
_target: {
browser: {
- ownerGlobal: { openTrustedLinkIn, whereToOpenLink: () => "current" },
+ ownerGlobal: { openTrustedLinkIn },
},
},
};
@@ -524,7 +524,7 @@ add_task(async function test_onAction_OPEN_LINK_pocket() {
},
_target: {
browser: {
- ownerGlobal: { openTrustedLinkIn, whereToOpenLink: () => "current" },
+ ownerGlobal: { openTrustedLinkIn },
},
},
};
@@ -551,7 +551,7 @@ add_task(async function test_onAction_OPEN_LINK_not_http() {
data: { url: "file:///foo.com" },
_target: {
browser: {
- ownerGlobal: { openTrustedLinkIn, whereToOpenLink: () => "current" },
+ ownerGlobal: { openTrustedLinkIn },
},
},
};
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..247e08b333 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, {
@@ -2970,9 +2970,10 @@ add_task(async function test_ContileIntegration() {
Assert.ok(fetched);
// Both "foo" and "bar" should be filtered
- Assert.equal(feed._contile.sites.length, 2);
+ Assert.equal(feed._contile.sites.length, 3);
Assert.equal(feed._contile.sites[0].url, "https://www.test.com");
Assert.equal(feed._contile.sites[1].url, "https://test1.com");
+ Assert.equal(feed._contile.sites[2].url, "https://test2.com");
}
{
diff --git a/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js b/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js
index 5d13df0eb0..04501dbe53 100644
--- a/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js
+++ b/browser/components/newtab/test/xpcshell/test_TopSitesFeed_glean.js
@@ -47,6 +47,15 @@ let contileTile3 = {
image_size: 200,
impression_url: "https://impression_url.com",
};
+let contileTile4 = {
+ id: 75899,
+ name: "Brand4",
+ url: "https://www.brand4.com",
+ click_url: "https://click_url.com",
+ image_url: "https://contile-images.jpg",
+ image_size: 200,
+ impression_url: "https://impression_url.com",
+};
let mozSalesTile = [
{
label: "MozSales Title",
@@ -155,7 +164,12 @@ add_task(async function test_set_contile_tile_to_oversold() {
let feed = getTopSitesFeedForTest(sandbox);
feed._telemetryUtility.setSponsoredTilesConfigured();
- feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+ feed._telemetryUtility.setTiles([
+ contileTile1,
+ contileTile2,
+ contileTile3,
+ contileTile4,
+ ]);
let mergedTiles = [
{
@@ -170,12 +184,18 @@ add_task(async function test_set_contile_tile_to_oversold() {
sponsored_position: 2,
partner: "amp",
},
+ {
+ url: "https://www.brand3.com",
+ label: "brand3",
+ sponsored_position: 3,
+ partner: "amp",
+ },
];
feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
feed._telemetryUtility.finalizeNewtabPingFields(mergedTiles);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -194,6 +214,12 @@ add_task(async function test_set_contile_tile_to_oversold() {
{
advertiser: "brand3",
provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand4",
+ provider: "amp",
display_position: null,
display_fail_reason: "oversold",
},
@@ -477,7 +503,12 @@ add_task(async function test_set_tiles_to_dismissed_then_updated() {
feed._telemetryUtility.setSponsoredTilesConfigured();
// Step 1: Set initial tiles
- feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+ feed._telemetryUtility.setTiles([
+ contileTile1,
+ contileTile2,
+ contileTile3,
+ contileTile4,
+ ]);
// Step 2: Set all tiles to dismissed
feed._telemetryUtility.determineFilteredTilesAndSetToDismissed([]);
@@ -495,12 +526,18 @@ add_task(async function test_set_tiles_to_dismissed_then_updated() {
sponsored_position: 2,
partner: "amp",
},
+ {
+ url: "https://www.brand3.com",
+ label: "brand3",
+ sponsored_position: 3,
+ partner: "amp",
+ },
];
// Step 3: Finalize with the updated list of tiles.
feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -522,6 +559,12 @@ add_task(async function test_set_tiles_to_dismissed_then_updated() {
display_position: null,
display_fail_reason: "dismissed",
},
+ {
+ advertiser: "brand4",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "dismissed",
+ },
],
};
Assert.equal(
@@ -537,7 +580,12 @@ add_task(async function test_set_tile_positions_after_updated_list() {
feed._telemetryUtility.setSponsoredTilesConfigured();
// Step 1: Set initial tiles
- feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+ feed._telemetryUtility.setTiles([
+ contileTile1,
+ contileTile2,
+ contileTile3,
+ contileTile4,
+ ]);
// Step 2: Set 1 tile to oversold (brand3)
let mergedTiles = [
@@ -553,6 +601,12 @@ add_task(async function test_set_tile_positions_after_updated_list() {
sponsored_position: 2,
partner: "amp",
},
+ {
+ url: "https://www.brand3.com",
+ label: "brand3",
+ sponsored_position: 3,
+ partner: "amp",
+ },
];
feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
@@ -570,10 +624,16 @@ add_task(async function test_set_tile_positions_after_updated_list() {
sponsored_position: 2,
partner: "amp",
},
+ {
+ url: "https://www.brand3.com",
+ label: "brand3",
+ sponsored_position: 3,
+ partner: "amp",
+ },
];
feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -592,6 +652,12 @@ add_task(async function test_set_tile_positions_after_updated_list() {
{
advertiser: "brand3",
provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand4",
+ provider: "amp",
display_position: null,
display_fail_reason: "oversold",
},
@@ -610,7 +676,12 @@ add_task(async function test_set_tile_positions_after_updated_list_all_tiles() {
feed._telemetryUtility.setSponsoredTilesConfigured();
// Step 1: Set initial tiles
- feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+ feed._telemetryUtility.setTiles([
+ contileTile1,
+ contileTile2,
+ contileTile3,
+ contileTile4,
+ ]);
// Step 2: Set 1 tile to oversold (brand3)
let mergedTiles = [
@@ -626,6 +697,12 @@ add_task(async function test_set_tile_positions_after_updated_list_all_tiles() {
sponsored_position: 2,
partner: "amp",
},
+ {
+ url: "https://www.brand3.com",
+ label: "brand3",
+ sponsored_position: 3,
+ partner: "amp",
+ },
];
feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
@@ -643,10 +720,16 @@ add_task(async function test_set_tile_positions_after_updated_list_all_tiles() {
sponsored_position: 2,
partner: "amp",
},
+ {
+ url: "https://www.replacement3.com",
+ label: "replacement3",
+ sponsored_position: 3,
+ partner: "amp",
+ },
];
feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -665,6 +748,12 @@ add_task(async function test_set_tile_positions_after_updated_list_all_tiles() {
{
advertiser: "brand3",
provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand4",
+ provider: "amp",
display_position: null,
display_fail_reason: "oversold",
},
@@ -684,7 +773,12 @@ add_task(
feed._telemetryUtility.setSponsoredTilesConfigured();
// Step 1: Set initial tiles
- feed._telemetryUtility.setTiles([contileTile1, contileTile2, contileTile3]);
+ feed._telemetryUtility.setTiles([
+ contileTile1,
+ contileTile2,
+ contileTile3,
+ contileTile4,
+ ]);
// Step 2: Set 1 tile to oversold (brand3)
let mergedTiles = [
@@ -700,6 +794,12 @@ add_task(
sponsored_position: 2,
partner: "amp",
},
+ {
+ url: "https://www.brand3.com",
+ label: "brand3",
+ sponsored_position: 3,
+ partner: "amp",
+ },
];
feed._telemetryUtility.determineFilteredTilesAndSetToOversold(mergedTiles);
@@ -717,10 +817,16 @@ add_task(
sponsored_position: 2,
partner: "amp",
},
+ {
+ url: "https://www.brand3.com",
+ label: "brand3",
+ sponsored_position: 3,
+ partner: "amp",
+ },
];
feed._telemetryUtility.finalizeNewtabPingFields(updatedTiles);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -739,6 +845,12 @@ add_task(
{
advertiser: "brand3",
provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand4",
+ provider: "amp",
display_position: null,
display_fail_reason: "oversold",
},
@@ -901,7 +1013,7 @@ add_task(async function test_all_tiles_displayed() {
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -961,18 +1073,25 @@ add_task(async function test_set_one_tile_display_fail_reason_to_oversold() {
impression_url: "https://www.brand3-impression.com",
name: "brand3",
},
+ {
+ url: "https://www.brand4.com",
+ image_url: "images/brnad4-com.png",
+ click_url: "https://www.brand4-click.com",
+ impression_url: "https://www.brand4-impression.com",
+ name: "brand4",
+ },
],
}),
});
const fetched = await feed._contile._fetchSites();
Assert.ok(fetched);
- Assert.equal(feed._contile.sites.length, 2);
+ Assert.equal(feed._contile.sites.length, 3);
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
{
@@ -990,6 +1109,12 @@ add_task(async function test_set_one_tile_display_fail_reason_to_oversold() {
{
advertiser: "brand3",
provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand4",
+ provider: "amp",
display_position: null,
display_fail_reason: "oversold",
},
@@ -1052,7 +1177,7 @@ add_task(async function test_set_one_tile_display_fail_reason_to_dismissed() {
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -1129,6 +1254,13 @@ add_task(
impression_url: "https://www.brand4-impression.com",
name: "brand4",
},
+ {
+ url: "https://www.brand5.com",
+ image_url: "images/brand5-com.png",
+ click_url: "https://www.brand5-click.com",
+ impression_url: "https://www.brand5-impression.com",
+ name: "brand5",
+ },
],
}),
});
@@ -1140,12 +1272,12 @@ add_task(
const fetched = await feed._contile._fetchSites();
Assert.ok(fetched);
- Assert.equal(feed._contile.sites.length, 2);
+ Assert.equal(feed._contile.sites.length, 3);
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -1170,6 +1302,12 @@ add_task(
{
advertiser: "brand4",
provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand5",
+ provider: "amp",
display_position: null,
display_fail_reason: "oversold",
},
@@ -1233,7 +1371,7 @@ add_task(
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -1297,6 +1435,13 @@ add_task(async function test_update_tile_count() {
impression_url: "https://www.brand3-impression.com",
name: "brand3",
},
+ {
+ url: "https://www.brand4.com",
+ image_url: "images/brand4-com.png",
+ click_url: "https://www.brand4-click.com",
+ impression_url: "https://www.brand4-impression.com",
+ name: "brand4",
+ },
],
}),
});
@@ -1304,11 +1449,11 @@ add_task(async function test_update_tile_count() {
// 1. Initially the Nimbus pref is set to 2 tiles
let fetched = await feed._contile._fetchSites();
Assert.ok(fetched);
- Assert.equal(feed._contile.sites.length, 2);
+ Assert.equal(feed._contile.sites.length, 3);
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -1327,6 +1472,12 @@ add_task(async function test_update_tile_count() {
{
advertiser: "brand3",
provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand4",
+ provider: "amp",
display_position: null,
display_fail_reason: "oversold",
},
@@ -1344,7 +1495,7 @@ add_task(async function test_update_tile_count() {
);
setNimbusVariablesForNumTiles(nimbusPocketStub, 3);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
expectedResult = {
sponsoredTilesReceived: [
@@ -1363,6 +1514,12 @@ add_task(async function test_update_tile_count() {
{
advertiser: "brand3",
provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand4",
+ provider: "amp",
display_position: null,
display_fail_reason: "oversold",
},
@@ -1402,6 +1559,12 @@ add_task(async function test_update_tile_count() {
display_position: 3,
display_fail_reason: null,
},
+ {
+ advertiser: "brand4",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
],
};
Assert.equal(
@@ -1442,6 +1605,13 @@ add_task(async function test_update_tile_count_sourced_from_cache() {
impression_url: "https://www.brand3-impression.com",
name: "brand3",
},
+ {
+ url: "https://www.brand4.com",
+ image_url: "images/brand4-com.png",
+ click_url: "https://www.brand4-click.com",
+ impression_url: "https://www.brand4-impression.com",
+ name: "brand4",
+ },
];
Services.prefs.setStringPref(CONTILE_CACHE_PREF, JSON.stringify(tiles));
@@ -1459,11 +1629,11 @@ add_task(async function test_update_tile_count_sourced_from_cache() {
// Ensure ContileIntegration._fetchSites is working populate _sites and initilize TelemetryUtility
let fetched = await feed._contile._fetchSites();
Assert.ok(fetched);
- Assert.equal(feed._contile.sites.length, 3);
+ Assert.equal(feed._contile.sites.length, 4);
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -1482,6 +1652,12 @@ add_task(async function test_update_tile_count_sourced_from_cache() {
{
advertiser: "brand3",
provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
+ advertiser: "brand4",
+ provider: "amp",
display_position: null,
display_fail_reason: "oversold",
},
@@ -1499,12 +1675,12 @@ add_task(async function test_update_tile_count_sourced_from_cache() {
);
setNimbusVariablesForNumTiles(nimbusPocketStub, 3);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
// 3. Confirm the new count is applied when data pulled from Contile, 3 tiles displayed
fetched = await feed._contile._fetchSites();
Assert.ok(fetched);
- Assert.equal(feed._contile.sites.length, 3);
+ Assert.equal(feed._contile.sites.length, 4);
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
@@ -1530,6 +1706,12 @@ add_task(async function test_update_tile_count_sourced_from_cache() {
display_position: 3,
display_fail_reason: null,
},
+ {
+ advertiser: "brand4",
+ provider: "amp",
+ display_position: null,
+ display_fail_reason: "oversold",
+ },
],
};
Assert.equal(
@@ -1595,7 +1777,7 @@ add_task(
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -1636,7 +1818,7 @@ add_task(
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
expectedResult = {
sponsoredTilesReceived: [
@@ -1684,7 +1866,7 @@ add_task(async function test_sponsoredTilesReceived_not_set() {
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = { sponsoredTilesReceived: [] };
Assert.equal(
@@ -1744,7 +1926,7 @@ add_task(async function test_telemetry_data_updates() {
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -1896,7 +2078,7 @@ add_task(async function test_reset_telemetry_data() {
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
@@ -1934,7 +2116,7 @@ add_task(async function test_reset_telemetry_data() {
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
expectedResult = { sponsoredTilesReceived: [] };
Assert.equal(
@@ -1982,17 +2164,24 @@ add_task(async function test_set_telemetry_for_moz_sales_tiles() {
impression_url: "https://www.brand2-impression.com",
name: "brand2",
},
+ {
+ url: "https://www.brand3.com",
+ image_url: "images/brand3-com.png",
+ click_url: "https://www.brand3-click.com",
+ impression_url: "https://www.brand3-impression.com",
+ name: "brand2",
+ },
],
}),
});
const fetched = await feed._contile._fetchSites();
Assert.ok(fetched);
- Assert.equal(feed._contile.sites.length, 2);
+ Assert.equal(feed._contile.sites.length, 3);
await feed._readDefaults();
await feed.getLinksWithDefaults(false);
- Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 2);
+ Assert.equal(Glean.topsites.sponsoredTilesConfigured.testGetValue(), 3);
let expectedResult = {
sponsoredTilesReceived: [
{
@@ -2008,6 +2197,12 @@ add_task(async function test_set_telemetry_for_moz_sales_tiles() {
display_fail_reason: null,
},
{
+ advertiser: "brand3",
+ provider: "amp",
+ display_position: 3,
+ display_fail_reason: null,
+ },
+ {
advertiser: "mozsales title",
provider: "moz-sales",
display_position: null,
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/test_WeatherFeed.js b/browser/components/newtab/test/xpcshell/test_WeatherFeed.js
new file mode 100644
index 0000000000..2821f4b7d0
--- /dev/null
+++ b/browser/components/newtab/test/xpcshell/test_WeatherFeed.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { WeatherFeed } = ChromeUtils.importESModule(
+ "resource://activity-stream/lib/WeatherFeed.sys.mjs"
+);
+
+const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule(
+ "resource://activity-stream/common/Actions.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+ MerinoTestUtils: "resource://testing-common/MerinoTestUtils.sys.mjs",
+});
+
+const { WEATHER_SUGGESTION } = MerinoTestUtils;
+
+const WEATHER_ENABLED = "browser.newtabpage.activity-stream.showWeather";
+const SYS_WEATHER_ENABLED =
+ "browser.newtabpage.activity-stream.system.showWeather";
+
+add_task(async function test_construction() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(WeatherFeed.prototype, "PersistentCache").returns({
+ set: () => {},
+ get: () => {},
+ });
+
+ let feed = new WeatherFeed();
+
+ info("WeatherFeed constructor should create initial values");
+
+ Assert.ok(feed, "Could construct a WeatherFeed");
+ Assert.ok(feed.loaded === false, "WeatherFeed is not loaded");
+ Assert.ok(feed.merino === null, "merino is initialized as null");
+ Assert.ok(
+ feed.suggestions.length === 0,
+ "suggestions is initialized as a array with length of 0"
+ );
+ Assert.ok(feed.fetchTimer === null, "fetchTimer is initialized as null");
+ sandbox.restore();
+});
+
+add_task(async function test_onAction_INIT() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(WeatherFeed.prototype, "MerinoClient").returns({
+ get: () => [WEATHER_SUGGESTION],
+ on: () => {},
+ });
+ sandbox.stub(WeatherFeed.prototype, "PersistentCache").returns({
+ set: () => {},
+ get: () => {},
+ });
+ const dateNowTestValue = 1;
+ sandbox.stub(WeatherFeed.prototype, "Date").returns({
+ now: () => dateNowTestValue,
+ });
+
+ let feed = new WeatherFeed();
+
+ Services.prefs.setBoolPref(WEATHER_ENABLED, true);
+ Services.prefs.setBoolPref(SYS_WEATHER_ENABLED, true);
+
+ sandbox.stub(feed, "isEnabled").returns(true);
+
+ sandbox.stub(feed, "fetchHelper");
+ feed.suggestions = [WEATHER_SUGGESTION];
+
+ feed.store = {
+ dispatch: sinon.spy(),
+ };
+
+ info("WeatherFeed.onAction INIT should initialize Weather");
+
+ await feed.onAction({
+ type: at.INIT,
+ });
+
+ Assert.ok(feed.store.dispatch.calledOnce);
+ Assert.ok(
+ feed.store.dispatch.calledWith(
+ ac.BroadcastToContent({
+ type: at.WEATHER_UPDATE,
+ data: {
+ suggestions: [WEATHER_SUGGESTION],
+ lastUpdated: dateNowTestValue,
+ },
+ meta: {
+ isStartup: true,
+ },
+ })
+ )
+ );
+ Services.prefs.clearUserPref(WEATHER_ENABLED);
+ sandbox.restore();
+});
diff --git a/browser/components/newtab/test/xpcshell/xpcshell.toml b/browser/components/newtab/test/xpcshell/xpcshell.toml
index 87d73669d3..567927c31c 100644
--- a/browser/components/newtab/test/xpcshell/xpcshell.toml
+++ b/browser/components/newtab/test/xpcshell/xpcshell.toml
@@ -26,3 +26,7 @@ support-files = ["../schemas/*.schema.json"]
["test_TopSitesFeed.js"]
["test_TopSitesFeed_glean.js"]
+
+["test_WallpaperFeed.js"]
+
+["test_WeatherFeed.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
deleted file mode 100644
index 68dee3cf2b..0000000000
--- a/browser/components/originattributes/test/browser/file_thirdPartyChild.video.ogv
+++ /dev/null
Binary files differ
diff --git a/browser/components/originattributes/test/browser/file_thirdPartyChild.video.webm b/browser/components/originattributes/test/browser/file_thirdPartyChild.video.webm
new file mode 100644
index 0000000000..5ad699fc1a
--- /dev/null
+++ b/browser/components/originattributes/test/browser/file_thirdPartyChild.video.webm
Binary files differ
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/PlacesUIUtils.sys.mjs b/browser/components/places/PlacesUIUtils.sys.mjs
index b9e9efe70e..288877b55d 100644
--- a/browser/components/places/PlacesUIUtils.sys.mjs
+++ b/browser/components/places/PlacesUIUtils.sys.mjs
@@ -12,6 +12,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
CLIENT_NOT_CONFIGURED: "resource://services-sync/constants.sys.mjs",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
@@ -199,22 +200,25 @@ let InternalFaviconLoader = {
win.addEventListener("unload", unloadHandler, true);
}
- // First we do the actual setAndFetch call:
- let loadType = lazy.PrivateBrowsingUtils.isWindowPrivate(win)
- ? lazy.PlacesUtils.favicons.FAVICON_LOAD_PRIVATE
- : lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE;
let callback = this._makeCompletionCallback(win, innerWindowID);
-
- if (iconURI && iconURI.schemeIs("data")) {
- expiration = lazy.PlacesUtils.toPRTime(expiration);
- lazy.PlacesUtils.favicons.replaceFaviconDataFromDataURL(
+ if (iconURI?.schemeIs("data")) {
+ lazy.PlacesUtils.favicons.setFaviconForPage(
+ pageURI,
uri,
- iconURI.spec,
- expiration,
- principal
+ iconURI,
+ lazy.PlacesUtils.toPRTime(expiration),
+ () => {
+ callback.onComplete(uri);
+ }
);
+ return;
}
+ // First we do the actual setAndFetch call:
+ let loadType = lazy.PrivateBrowsingUtils.isWindowPrivate(win)
+ ? lazy.PlacesUtils.favicons.FAVICON_LOAD_PRIVATE
+ : lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE;
+
let request = lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
pageURI,
uri,
@@ -464,7 +468,7 @@ class BookmarkState {
})
);
break;
- case "tags":
+ case "tags": {
const newTags = value.filter(
tag => !this._originalState.tags.includes(tag)
);
@@ -488,6 +492,7 @@ class BookmarkState {
);
}
break;
+ }
case "keyword":
transactions.push(
lazy.PlacesTransactions.EditKeyword({
@@ -982,7 +987,7 @@ export var PlacesUIUtils = {
// whereToOpenLink doesn't return "window" when there's no browser window
// open (Bug 630255).
var where = browserWindow
- ? browserWindow.whereToOpenLink(aEvent, false, true)
+ ? lazy.BrowserUtils.whereToOpenLink(aEvent, false, true)
: "window";
if (where == "window") {
// There is no browser window open, thus open a new one.
@@ -1073,7 +1078,7 @@ export var PlacesUIUtils = {
openNodeWithEvent: function PUIU_openNodeWithEvent(aNode, aEvent) {
let window = aEvent.target.ownerGlobal;
- let where = window.whereToOpenLink(aEvent, false, true);
+ let where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true);
if (this.loadBookmarksInTabs && lazy.PlacesUtils.nodeIsBookmark(aNode)) {
if (where == "current" && !aNode.uri.startsWith("javascript:")) {
where = "tab";
@@ -1687,7 +1692,7 @@ export var PlacesUIUtils = {
doCommand(command) {
let window = this.triggerNode.ownerGlobal;
switch (command) {
- case "placesCmd_copy":
+ case "placesCmd_copy": {
// This is a little hacky, but there is a lot of code in Places that handles
// clipboard stuff, so it's easier to reuse.
let node = {};
@@ -1732,6 +1737,7 @@ export var PlacesUIUtils = {
Ci.nsIClipboard.kGlobalClipboard
);
break;
+ }
case "placesCmd_open:privatewindow":
window.openTrustedLinkIn(this.triggerNode.link, "window", {
private: true,
diff --git a/browser/components/places/content/controller.js b/browser/components/places/content/controller.js
index 6eaa129961..f46d696908 100644
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -1436,7 +1436,7 @@ PlacesController.prototype = {
let documentUrl = document.documentURI.toLowerCase();
if (documentUrl.endsWith("browser.xhtml")) {
// We're in a menu or a panel.
- window.SidebarUI._show("viewBookmarksSidebar").then(() => {
+ window.SidebarController._show("viewBookmarksSidebar").then(() => {
let theSidebar = document.getElementById("sidebar");
theSidebar.contentDocument
.getElementById("bookmarks-view")
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..f2f47ff4b4 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}`
);
}
@@ -386,7 +386,9 @@ add_task(async function test_sidebar_folder_contextmenu_contents() {
tree.selectItems([folder.guid]);
let contextMenu =
- SidebarUI.browser.contentDocument.getElementById("placesContext");
+ SidebarController.browser.contentDocument.getElementById(
+ "placesContext"
+ );
let popupShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popupshown"
@@ -396,7 +398,7 @@ add_task(async function test_sidebar_folder_contextmenu_contents() {
return contextMenu;
},
optionItems,
- SidebarUI.browser.contentDocument
+ SidebarController.browser.contentDocument
);
});
});
@@ -430,7 +432,9 @@ add_task(async function test_sidebar_multiple_folders_contextmenu_contents() {
tree.selectItems([folder1.guid, folder2.guid]);
let contextMenu =
- SidebarUI.browser.contentDocument.getElementById("placesContext");
+ SidebarController.browser.contentDocument.getElementById(
+ "placesContext"
+ );
let popupShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popupshown"
@@ -440,7 +444,7 @@ add_task(async function test_sidebar_multiple_folders_contextmenu_contents() {
return contextMenu;
},
optionItems,
- SidebarUI.browser.contentDocument
+ SidebarController.browser.contentDocument
);
});
});
@@ -473,7 +477,9 @@ add_task(async function test_sidebar_bookmark_contextmenu_contents() {
tree.selectItems([bookmark.guid]);
let contextMenu =
- SidebarUI.browser.contentDocument.getElementById("placesContext");
+ SidebarController.browser.contentDocument.getElementById(
+ "placesContext"
+ );
let popupShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popupshown"
@@ -483,7 +489,7 @@ add_task(async function test_sidebar_bookmark_contextmenu_contents() {
return contextMenu;
},
optionItems,
- SidebarUI.browser.contentDocument
+ SidebarController.browser.contentDocument
);
});
});
@@ -513,13 +519,17 @@ add_task(async function test_sidebar_bookmark_search_contextmenu_contents() {
info("Checking bookmark sidebar menu contents in search context");
// Perform a search first
let searchBox =
- SidebarUI.browser.contentDocument.getElementById("search-box");
+ SidebarController.browser.contentDocument.getElementById(
+ "search-box"
+ );
searchBox.value = SECOND_BOOKMARK_TITLE;
searchBox.doCommand();
tree.selectItems([bookmark.guid]);
let contextMenu =
- SidebarUI.browser.contentDocument.getElementById("placesContext");
+ SidebarController.browser.contentDocument.getElementById(
+ "placesContext"
+ );
let popupShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popupshown"
@@ -529,7 +539,7 @@ add_task(async function test_sidebar_bookmark_search_contextmenu_contents() {
return contextMenu;
},
optionItems,
- SidebarUI.browser.contentDocument
+ SidebarController.browser.contentDocument
);
});
});
@@ -641,7 +651,9 @@ add_task(async function test_sidebar_mixedselection_contextmenu_contents() {
tree.selectItems([bookmark.guid, folder.guid]);
let contextMenu =
- SidebarUI.browser.contentDocument.getElementById("placesContext");
+ SidebarController.browser.contentDocument.getElementById(
+ "placesContext"
+ );
let popupShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popupshown"
@@ -651,7 +663,7 @@ add_task(async function test_sidebar_mixedselection_contextmenu_contents() {
return contextMenu;
},
optionItems,
- SidebarUI.browser.contentDocument
+ SidebarController.browser.contentDocument
);
});
});
@@ -679,7 +691,9 @@ add_task(async function test_sidebar_multiple_bookmarks_contextmenu_contents() {
tree.selectItems([bookmark.guid, bookmark2.guid]);
let contextMenu =
- SidebarUI.browser.contentDocument.getElementById("placesContext");
+ SidebarController.browser.contentDocument.getElementById(
+ "placesContext"
+ );
let popupShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popupshown"
@@ -689,7 +703,7 @@ add_task(async function test_sidebar_multiple_bookmarks_contextmenu_contents() {
return contextMenu;
},
optionItems,
- SidebarUI.browser.contentDocument
+ SidebarController.browser.contentDocument
);
});
});
@@ -714,7 +728,9 @@ add_task(async function test_sidebar_multiple_links_contextmenu_contents() {
tree.selectAll();
let contextMenu =
- SidebarUI.browser.contentDocument.getElementById("placesContext");
+ SidebarController.browser.contentDocument.getElementById(
+ "placesContext"
+ );
let popupShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popupshown"
@@ -724,7 +740,7 @@ add_task(async function test_sidebar_multiple_links_contextmenu_contents() {
return contextMenu;
},
optionItems,
- SidebarUI.browser.contentDocument
+ SidebarController.browser.contentDocument
);
});
});
@@ -750,7 +766,9 @@ add_task(async function test_sidebar_mixed_bookmarks_contextmenu_contents() {
tree.selectItems([bookmark.guid, folder.guid]);
let contextMenu =
- SidebarUI.browser.contentDocument.getElementById("placesContext");
+ SidebarController.browser.contentDocument.getElementById(
+ "placesContext"
+ );
let popupShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
"popupshown"
@@ -760,7 +778,7 @@ add_task(async function test_sidebar_mixed_bookmarks_contextmenu_contents() {
return contextMenu;
},
optionItems,
- SidebarUI.browser.contentDocument
+ SidebarController.browser.contentDocument
);
});
});
diff --git a/browser/components/places/tests/browser/browser_bookmarksProperties.js b/browser/components/places/tests/browser/browser_bookmarksProperties.js
index cfa9e6c581..b1147c3541 100644
--- a/browser/components/places/tests/browser/browser_bookmarksProperties.js
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -156,7 +156,7 @@ gTests.push({
},
finish() {
- SidebarUI.hide();
+ SidebarController.hide();
},
async cleanup() {
@@ -282,7 +282,7 @@ gTests.push({
},
finish() {
- SidebarUI.hide();
+ SidebarController.hide();
},
async cleanup() {
@@ -399,7 +399,7 @@ gTests.push({
},
finish() {
- SidebarUI.hide();
+ SidebarController.hide();
},
async cleanup() {
@@ -450,7 +450,7 @@ function execute_test_in_sidebar(test) {
},
{ capture: true, once: true }
);
- SidebarUI.show(test.sidebar);
+ SidebarController.show(test.sidebar);
});
}
diff --git a/browser/components/places/tests/browser/browser_check_correct_controllers.js b/browser/components/places/tests/browser/browser_check_correct_controllers.js
index 80095823e1..d19939b98b 100644
--- a/browser/components/places/tests/browser/browser_check_correct_controllers.js
+++ b/browser/components/places/tests/browser/browser_check_correct_controllers.js
@@ -32,7 +32,7 @@ add_task(async function test() {
let sidebar = await promiseLoadedSidebar("viewBookmarksSidebar");
registerCleanupFunction(() => {
- SidebarUI.hide();
+ SidebarController.hide();
});
// Focus the tree and check if its controller is returned.
@@ -109,6 +109,6 @@ function promiseLoadedSidebar(cmd) {
{ capture: true, once: true }
);
- SidebarUI.show(cmd);
+ SidebarController.show(cmd);
});
}
diff --git a/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js b/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js
index 8d4d650984..47c53e6027 100644
--- a/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js
+++ b/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js
@@ -11,7 +11,7 @@
registerCleanupFunction(async () => {
CustomizableUI.setToolbarVisibility("PersonalToolbar", false);
CustomizableUI.removeWidgetFromArea("library-button");
- SidebarUI.hide();
+ SidebarController.hide();
});
async function selectAppMenuView(buttonId, viewId) {
diff --git a/browser/components/places/tests/browser/browser_sidebar_on_customization.js b/browser/components/places/tests/browser/browser_sidebar_on_customization.js
index 6e97f81dd3..e5704e9d04 100644
--- a/browser/components/places/tests/browser/browser_sidebar_on_customization.js
+++ b/browser/components/places/tests/browser/browser_sidebar_on_customization.js
@@ -30,9 +30,9 @@ add_setup(async function () {
add_task(async function test_open_sidebar_and_customize() {
await withSidebarTree("bookmarks", async tree => {
async function checkTreeIsFunctional() {
- Assert.ok(SidebarUI.isOpen, "Sidebar is open");
+ Assert.ok(SidebarController.isOpen, "Sidebar is open");
Assert.ok(
- BrowserTestUtils.isVisible(SidebarUI.browser),
+ BrowserTestUtils.isVisible(SidebarController.browser),
"sidebar browser is visible"
);
Assert.ok(tree.view.result, "View result is defined");
@@ -49,7 +49,7 @@ add_task(async function test_open_sidebar_and_customize() {
await promiseCustomizeStart();
Assert.ok(
- !BrowserTestUtils.isVisible(SidebarUI.browser),
+ !BrowserTestUtils.isVisible(SidebarController.browser),
"sidebar browser is hidden"
);
Assert.ok(tree.view.result, "View result is defined");
diff --git a/browser/components/places/tests/browser/browser_sidebarpanels_click.js b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
index 3e5b1c6ec6..f107eb76d5 100644
--- a/browser/components/places/tests/browser/browser_sidebarpanels_click.js
+++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
@@ -31,7 +31,7 @@ add_task(async function test_sidebarpanels_click() {
false,
"Unexpected sidebar found - a previous test failed to cleanup correctly"
);
- SidebarUI.hide();
+ SidebarController.hide();
}
// Ensure history is clean before starting the test.
@@ -137,7 +137,7 @@ async function testPlacesPanel(testInfo) {
await promiseAlert;
executeSoon(async function () {
- SidebarUI.hide();
+ SidebarController.hide();
await testInfo.cleanup();
resolve();
});
@@ -147,7 +147,7 @@ async function testPlacesPanel(testInfo) {
);
});
- SidebarUI.show(testInfo.sidebarName);
+ SidebarController.show(testInfo.sidebarName);
return promise;
}
@@ -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/browser_toolbarbutton_menu_show_in_folder.js b/browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js
index 02720cfa2e..c4f6912044 100644
--- a/browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js
+++ b/browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js
@@ -11,7 +11,7 @@ const TEST_TITLE = "Test Bookmark";
let appMenuButton = document.getElementById("PanelUI-menu-button");
let bookmarksAppMenu = document.getElementById("PanelUI-bookmarks");
-let sidebarWasAlreadyOpen = SidebarUI.isOpen;
+let sidebarWasAlreadyOpen = SidebarController.isOpen;
const { CustomizableUITestUtils } = ChromeUtils.importESModule(
"resource://testing-common/CustomizableUITestUtils.sys.mjs"
@@ -86,7 +86,7 @@ add_task(async function toolbarBookmarkShowInFolder() {
// Cleanup
await PlacesUtils.bookmarks.eraseEverything();
if (!sidebarWasAlreadyOpen) {
- SidebarUI.hide();
+ SidebarController.hide();
}
await gCUITestUtils.hideMainMenu();
});
diff --git a/browser/components/places/tests/browser/browser_views_iconsupdate.js b/browser/components/places/tests/browser/browser_views_iconsupdate.js
index 1799a9665b..d1e5ea83ea 100644
--- a/browser/components/places/tests/browser/browser_views_iconsupdate.js
+++ b/browser/components/places/tests/browser/browser_views_iconsupdate.js
@@ -28,9 +28,9 @@ add_task(async function () {
let promiseSidebarLoaded = new Promise(resolve => {
sidebar.addEventListener("load", resolve, { capture: true, once: true });
});
- SidebarUI.show("viewBookmarksSidebar");
+ SidebarController.show("viewBookmarksSidebar");
registerCleanupFunction(() => {
- SidebarUI.hide();
+ SidebarController.hide();
});
await promiseSidebarLoaded;
diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js
index bcd89bce15..2decd11da9 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;
}
@@ -395,7 +395,7 @@ var withSidebarTree = async function (type, taskFn) {
});
let sidebarId =
type == "bookmarks" ? "viewBookmarksSidebar" : "viewHistorySidebar";
- SidebarUI.show(sidebarId);
+ SidebarController.show(sidebarId);
await sidebarLoadedPromise;
let treeId = type == "bookmarks" ? "bookmarks-view" : "historyTree";
@@ -406,7 +406,7 @@ var withSidebarTree = async function (type, taskFn) {
try {
await taskFn(tree);
} finally {
- SidebarUI.hide();
+ SidebarController.hide();
}
};
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/dialogs/connection.js b/browser/components/preferences/dialogs/connection.js
index 33e8deb279..6d2623d65f 100644
--- a/browser/components/preferences/dialogs/connection.js
+++ b/browser/components/preferences/dialogs/connection.js
@@ -132,10 +132,11 @@ var gConnectionsDialog = {
if ("@mozilla.org/system-proxy-settings;1" in Cc) {
document.getElementById("systemPref").removeAttribute("hidden");
- var systemWpadAllowed = Preferences.get(
- "network.proxy.system_wpad.allowed"
+ var systemWpadAllowed = Services.prefs.getBoolPref(
+ "network.proxy.system_wpad.allowed",
+ false
);
- if (systemWpadAllowed && Services.appinfo.OS == "WINNT") {
+ if (systemWpadAllowed && AppConstants.platform == "win") {
document.getElementById("systemWpad").removeAttribute("hidden");
}
}
diff --git a/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml
index 2b041c4802..83bd09c0c3 100644
--- a/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml
+++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml
@@ -26,7 +26,6 @@
rel="localization"
href="browser/preferences/preferences.ftl"
/>
- <html:link rel="localization" href="toolkit/branding/accounts.ftl" />
</linkset>
<script src="chrome://global/content/preferencesBindings.js" />
<script src="chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.js" />
diff --git a/browser/components/preferences/fxaPairDevice.xhtml b/browser/components/preferences/fxaPairDevice.xhtml
index ad068b143d..ce988d289a 100644
--- a/browser/components/preferences/fxaPairDevice.xhtml
+++ b/browser/components/preferences/fxaPairDevice.xhtml
@@ -28,7 +28,6 @@
rel="localization"
href="browser/preferences/fxaPairDevice.ftl"
/>
- <html:link rel="localization" href="toolkit/branding/accounts.ftl" />
</linkset>
<script src="chrome://browser/content/preferences/fxaPairDevice.js" />
diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml
index 1eb2189c41..fcf015a3b9 100644
--- a/browser/components/preferences/main.inc.xhtml
+++ b/browser/components/preferences/main.inc.xhtml
@@ -81,6 +81,14 @@
</hbox>
</groupbox>
+<groupbox id="dataBackupGroup" data-category="paneGeneral" hidden="true"
+ data-hidden-from-search="true">
+ <label><html:h2 data-l10n-id="settings-data-backup-header"/></label>
+ <hbox flex="1">
+ <html:backup-settings />
+ </hbox>
+</groupbox>
+
<!-- Tab preferences -->
<groupbox data-category="paneGeneral"
hidden="true">
@@ -384,7 +392,7 @@
<vbox id="translationsGroup" hidden="true" data-subcategory="translations">
<label><html:h2 data-l10n-id="translations-manage-header"/></label>
<hbox id="translations-manage-description" align="center">
- <description flex="1" data-l10n-id="translations-manage-intro"/>
+ <description flex="1" data-l10n-id="translations-manage-intro-2"/>
<button id="translations-manage-settings-button"
is="highlightable-button"
class="accessory-button"
@@ -393,9 +401,9 @@
<vbox>
<html:div id="translations-manage-install-list" hidden="true">
<hbox class="translations-manage-language">
- <label data-l10n-id="translations-manage-install-description"></label>
+ <label data-l10n-id="translations-manage-download-description"></label>
<button id="translations-manage-install-all"
- data-l10n-id="translations-manage-language-install-all-button"></button>
+ data-l10n-id="translations-manage-language-download-all-button"></button>
<button id="translations-manage-delete-all"
data-l10n-id="translations-manage-language-remove-all-button"></button>
</hbox>
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
index b578e0e29f..22bd3fe174 100644
--- a/browser/components/preferences/main.js
+++ b/browser/components/preferences/main.js
@@ -430,19 +430,29 @@ var gMainPane = {
"command",
gMainPane.onWindowsLaunchOnLoginChange
);
- NimbusFeatures.windowsLaunchOnLogin.recordExposureEvent({
- once: true,
- });
// We do a check here for startWithLastProfile as we could
// have disabled the pref for the user before they're ever
// exposed to the experiment on a new profile.
+ // If we're using MSIX, we don't show the checkbox as MSIX
+ // can't write to the registry.
if (
- NimbusFeatures.windowsLaunchOnLogin.getVariable("enabled") &&
Cc["@mozilla.org/toolkit/profile-service;1"].getService(
Ci.nsIToolkitProfileService
- ).startWithLastProfile
+ ).startWithLastProfile &&
+ !Services.sysinfo.getProperty("hasWinPackageId", false)
) {
- document.getElementById("windowsLaunchOnLoginBox").hidden = false;
+ NimbusFeatures.windowsLaunchOnLogin.recordExposureEvent({
+ once: true,
+ });
+
+ if (
+ Services.prefs.getBoolPref(
+ "browser.startup.windowsLaunchOnLogin.enabled",
+ false
+ )
+ ) {
+ document.getElementById("windowsLaunchOnLoginBox").hidden = false;
+ }
}
}
gMainPane.updateBrowserStartupUI =
@@ -521,6 +531,14 @@ var gMainPane = {
document.getElementById("dataMigrationGroup").remove();
}
+ if (
+ Services.prefs.getBoolPref("browser.backup.preferences.ui.enabled", false)
+ ) {
+ let backupGroup = document.getElementById("dataBackupGroup");
+ backupGroup.hidden = false;
+ backupGroup.removeAttribute("data-hidden-from-search");
+ }
+
// For media control toggle button, we support it on Windows, macOS and
// gtk-based Linux.
if (
@@ -1130,7 +1148,7 @@ var gMainPane = {
this.markAllDownloadPhases("downloaded");
} catch (error) {
TranslationsView.showError(
- "translations-manage-error-install",
+ "translations-manage-error-download",
error
);
await this.reloadDownloadPhases();
@@ -1167,7 +1185,7 @@ var gMainPane = {
this.updateDownloadPhase(langTag, "downloaded");
} catch (error) {
TranslationsView.showError(
- "translations-manage-error-install",
+ "translations-manage-error-download",
error
);
this.updateDownloadPhase(langTag, "uninstalled");
@@ -1221,7 +1239,7 @@ var gMainPane = {
document.l10n.setAttributes(
downloadButton,
- "translations-manage-language-install-button"
+ "translations-manage-language-download-button"
);
document.l10n.setAttributes(
deleteButton,
@@ -2237,9 +2255,8 @@ var gMainPane = {
// 1/2/4 values set via about:config should persist
return this._storedFullKeyboardNavigation;
}
- // When the checkbox is unchecked, this pref shouldn't exist
- // at all.
- return undefined;
+ // When the checkbox is unchecked, default to just text controls.
+ return 1;
},
/**
@@ -4202,7 +4219,7 @@ const AppearanceChooser = {
e.preventDefault();
break;
case "web-appearance-manage-themes-link":
- window.browsingContext.topChromeWindow.BrowserOpenAddonsMgr(
+ window.browsingContext.topChromeWindow.BrowserAddonUI.openAddonsMgr(
"addons://list/theme"
);
e.preventDefault();
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
index c30a51c67c..34d6a9b11d 100644
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
@@ -264,7 +264,7 @@ function init_all() {
return;
}
let mainWindow = window.browsingContext.topChromeWindow;
- mainWindow.BrowserOpenAddonsMgr();
+ mainWindow.BrowserAddonUI.openAddonsMgr();
});
document.dispatchEvent(
@@ -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) {
@@ -601,8 +575,9 @@ async function confirmRestartPrompt(
break;
}
- let buttonIndex = Services.prompt.confirmEx(
- window,
+ let button = await Services.prompt.asyncConfirmEx(
+ window.browsingContext,
+ Ci.nsIPrompt.MODAL_TYPE_CONTENT,
title,
msg,
buttonFlags,
@@ -613,6 +588,8 @@ async function confirmRestartPrompt(
{}
);
+ let buttonIndex = button.get("buttonNumClicked");
+
// If we have the second confirmation dialog for restart, see if the user
// cancels out at that point.
if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
index eee227822a..4b1a62b578 100644
--- a/browser/components/preferences/preferences.xhtml
+++ b/browser/components/preferences/preferences.xhtml
@@ -49,7 +49,6 @@
<link rel="localization" href="browser/preferences/fonts.ftl"/>
<link rel="localization" href="browser/preferences/moreFromMozilla.ftl"/>
<link rel="localization" href="browser/preferences/preferences.ftl"/>
- <link rel="localization" href="toolkit/branding/accounts.ftl"/>
<link rel="localization" href="toolkit/branding/brandings.ftl"/>
<link rel="localization" href="toolkit/featuregates/features.ftl"/>
@@ -69,6 +68,7 @@
<link rel="localization" href="browser/translations.ftl"/>
<link rel="localization" href="preview/translations.ftl"/>
<link rel="localization" href="preview/enUS-searchFeatures.ftl"/>
+ <link rel="localization" href="preview/backupSettings.ftl"/>
<link rel="localization" href="security/certificates/certManager.ftl"/>
<link rel="localization" href="security/certificates/deviceManager.ftl"/>
<link rel="localization" href="toolkit/updates/history.ftl"/>
@@ -85,6 +85,9 @@
<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>
+ <script type="module" src="chrome://browser/content/backup/backup-settings.mjs"></script>
</head>
<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
@@ -224,7 +227,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..e93a027a0f 100644
--- a/browser/components/preferences/privacy.inc.xhtml
+++ b/browser/components/preferences/privacy.inc.xhtml
@@ -552,6 +552,12 @@
</hbox>
</vbox>
<vbox>
+ <hbox id="osReauthRow" align="center">
+ <checkbox id="osReauthCheckbox"
+ data-l10n-id="forms-os-reauth"/>
+ </hbox>
+ </vbox>
+ <vbox>
<hbox id="masterPasswordRow" align="center">
<checkbox id="useMasterPassword"
data-l10n-id="forms-primary-pw-use"
@@ -1182,21 +1188,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..2d6fe7cacd 100644
--- a/browser/components/preferences/privacy.js
+++ b/browser/components/preferences/privacy.js
@@ -60,11 +60,15 @@ ChromeUtils.defineLazyGetter(this, "AlertsServiceDND", function () {
}
});
-XPCOMUtils.defineLazyPreferenceGetter(
- this,
- "OS_AUTH_ENABLED",
- "signon.management.page.os-auth.enabled",
- true
+ChromeUtils.defineLazyGetter(lazy, "AboutLoginsL10n", () => {
+ return new Localization(["branding/brand.ftl", "browser/aboutLogins.ftl"]);
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gParentalControlsService",
+ "@mozilla.org/parental-controls-service;1",
+ "nsIParentalControlsService"
);
XPCOMUtils.defineLazyPreferenceGetter(
@@ -682,11 +686,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 +709,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(
@@ -1034,6 +1050,7 @@ var gPrivacyPane = {
this._initPasswordGenerationUI();
this._initRelayIntegrationUI();
this._initMasterPasswordUI();
+ this._initOSAuthentication();
this.initListenersForExtensionControllingPasswordManager();
@@ -2844,8 +2861,7 @@ var gPrivacyPane = {
// OS reauthenticate functionality is not available on Linux yet (bug 1527745)
if (
!LoginHelper.isPrimaryPasswordSet() &&
- OS_AUTH_ENABLED &&
- OSKeyStore.canReauth()
+ LoginHelper.getOSAuthEnabled(LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF)
) {
// Uses primary-password-os-auth-dialog-message-win and
// primary-password-os-auth-dialog-message-macosx via concatenation:
@@ -2942,6 +2958,54 @@ var gPrivacyPane = {
this._updateRelayIntegrationUI();
},
+ async _toggleOSAuth() {
+ let osReauthCheckbox = document.getElementById("osReauthCheckbox");
+
+ const messageText = await lazy.AboutLoginsL10n.formatValue(
+ "about-logins-os-auth-dialog-message"
+ );
+ const captionText = await lazy.AboutLoginsL10n.formatValue(
+ "about-logins-os-auth-dialog-caption"
+ );
+ let win =
+ osReauthCheckbox.ownerGlobal.docShell.chromeEventHandler.ownerGlobal;
+
+ // Calling OSKeyStore.ensureLoggedIn() instead of LoginHelper.verifyOSAuth()
+ // since we want to authenticate user each time this stting is changed.
+ let isAuthorized = (
+ await OSKeyStore.ensureLoggedIn(messageText, captionText, win, false)
+ ).authenticated;
+ if (!isAuthorized) {
+ osReauthCheckbox.checked = !osReauthCheckbox.checked;
+ return;
+ }
+
+ // If osReauthCheckbox is checked enable osauth.
+ LoginHelper.setOSAuthEnabled(
+ LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF,
+ osReauthCheckbox.checked
+ );
+ },
+
+ _initOSAuthentication() {
+ let osReauthCheckbox = document.getElementById("osReauthCheckbox");
+ if (!OSKeyStore.canReauth()) {
+ osReauthCheckbox.hidden = true;
+ return;
+ }
+
+ osReauthCheckbox.setAttribute(
+ "checked",
+ LoginHelper.getOSAuthEnabled(LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF)
+ );
+
+ setEventListener(
+ "osReauthCheckbox",
+ "command",
+ gPrivacyPane._toggleOSAuth.bind(gPrivacyPane)
+ );
+ },
+
/**
* Shows the sites where the user has saved passwords and the associated login
* information.
@@ -3208,8 +3272,8 @@ var gPrivacyPane = {
initDataCollection() {
if (
!AppConstants.MOZ_DATA_REPORTING &&
- !NimbusFeatures.majorRelease2022.getVariable(
- "feltPrivacyShowPreferencesSection"
+ !Services.prefs.getBoolPref(
+ "browser.privacySegmentation.preferences.show"
)
) {
// Nothing to control in the data collection section, remove it.
@@ -3236,16 +3300,19 @@ var gPrivacyPane = {
// Section visibility
let section = document.getElementById("privacySegmentationSection");
let updatePrivacySegmentationSectionVisibilityState = () => {
- section.hidden = !NimbusFeatures.majorRelease2022.getVariable(
- "feltPrivacyShowPreferencesSection"
+ section.hidden = !Services.prefs.getBoolPref(
+ "browser.privacySegmentation.preferences.show"
);
};
- NimbusFeatures.majorRelease2022.onUpdate(
+ Services.prefs.addObserver(
+ "browser.privacySegmentation.preferences.show",
updatePrivacySegmentationSectionVisibilityState
);
+
window.addEventListener("unload", () => {
- NimbusFeatures.majorRelease2022.offUpdate(
+ Services.prefs.removeObserver(
+ "browser.privacySegmentation.preferences.show",
updatePrivacySegmentationSectionVisibilityState
);
});
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..07d9cc2880 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"]
@@ -67,6 +72,9 @@ skip-if = [
"verify && debug && os == 'mac'",
]
+["browser_connection_system_wpad.js"]
+run-if = ["os == 'win'"]
+
["browser_connection_valid_hostname.js"]
["browser_containers_name_input.js"]
@@ -276,7 +284,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"]
@@ -289,4 +296,4 @@ fail-if = ["a11y_checks"] # Bug 1854636 clicked label.dialogTitle, vbox#dialogTe
["browser_warning_permanent_private_browsing.js"]
["browser_windows_launch_on_login.js"]
-run-if = ["os == 'win'"]
+run-if = ["(os == 'win' && !msix)"] # Disabled for MSIX due to https://bugzilla.mozilla.org/show_bug.cgi?id=1888263
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_bug731866.js b/browser/components/preferences/tests/browser_bug731866.js
index b090535a49..e9ecd08a81 100644
--- a/browser/components/preferences/tests/browser_bug731866.js
+++ b/browser/components/preferences/tests/browser_bug731866.js
@@ -7,6 +7,9 @@ const browserContainersGroupDisabled = !SpecialPowers.getBoolPref(
const cookieBannerHandlingDisabled = !SpecialPowers.getBoolPref(
"cookiebanners.ui.desktop.enabled"
);
+const backupGroupDisabled = !SpecialPowers.getBoolPref(
+ "browser.backup.preferences.ui.enabled"
+);
const updatePrefContainers = ["updatesCategory", "updateApp"];
const updateContainersGroupDisabled =
AppConstants.platform === "win" &&
@@ -60,6 +63,12 @@ function checkElements(expectedPane) {
continue;
}
+ // Backup is currently disabled by default. (bug 1895791)
+ if (element.id == "dataBackupGroup" && backupGroupDisabled) {
+ is_element_hidden(element, "Disabled dataBackupGroup should be hidden");
+ continue;
+ }
+
let attributeValue = element.getAttribute("data-category");
let suffix = " (id=" + element.id + ")";
if (attributeValue == "pane" + expectedPane) {
diff --git a/browser/components/preferences/tests/browser_connection_system_wpad.js b/browser/components/preferences/tests/browser_connection_system_wpad.js
new file mode 100644
index 0000000000..87a3dbebae
--- /dev/null
+++ b/browser/components/preferences/tests/browser_connection_system_wpad.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_system_wpad() {
+ await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true });
+ const connectionURL =
+ "chrome://browser/content/preferences/dialogs/connection.xhtml";
+
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("network.proxy.system_wpad.allowed");
+ });
+
+ Services.prefs.setBoolPref("network.proxy.system_wpad.allowed", true);
+ let dialog = await openAndLoadSubDialog(connectionURL);
+ let dialogElement = dialog.document.getElementById("ConnectionsDialog");
+ let systemWpad = dialog.document.getElementById("systemWpad");
+ Assert.ok(!systemWpad.hidden, "Use system WPAD checkbox should be visible");
+ let dialogClosingPromise = BrowserTestUtils.waitForEvent(
+ dialogElement,
+ "dialogclosing"
+ );
+ dialogElement.cancelDialog();
+ await dialogClosingPromise;
+
+ Services.prefs.setBoolPref("network.proxy.system_wpad.allowed", false);
+ dialog = await openAndLoadSubDialog(connectionURL);
+ dialogElement = dialog.document.getElementById("ConnectionsDialog");
+ systemWpad = dialog.document.getElementById("systemWpad");
+ Assert.ok(systemWpad.hidden, "Use system WPAD checkbox should be hidden");
+ dialogClosingPromise = BrowserTestUtils.waitForEvent(
+ dialogElement,
+ "dialogclosing"
+ );
+ dialogElement.cancelDialog();
+ await dialogClosingPromise;
+
+ gBrowser.removeCurrentTab();
+});
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_keyboardfocus.js b/browser/components/preferences/tests/browser_keyboardfocus.js
index bed452b679..89576b926a 100644
--- a/browser/components/preferences/tests/browser_keyboardfocus.js
+++ b/browser/components/preferences/tests/browser_keyboardfocus.js
@@ -13,40 +13,42 @@ add_task(async function () {
let checkbox = gBrowser.contentDocument.querySelector(
"#useFullKeyboardNavigation"
);
- Assert.ok(
- !Services.prefs.getIntPref("accessibility.tabfocus", undefined),
- "no pref value should exist"
+ Assert.equal(
+ Services.prefs.getIntPref("accessibility.tabfocus"),
+ 7,
+ "default should be full keyboard access"
);
Assert.ok(
- !checkbox.checked,
- "checkbox should be unchecked before clicking on checkbox"
+ checkbox.checked,
+ "checkbox should be checked before clicking on checkbox"
);
checkbox.click();
Assert.equal(
Services.prefs.getIntPref("accessibility.tabfocus"),
- 7,
+ 1,
"Prefstore should reflect checkbox's associated numeric value"
);
Assert.ok(
- checkbox.checked,
- "checkbox should be checked after clicking on checkbox"
+ !checkbox.checked,
+ "checkbox should be unchecked after clicking on checkbox"
);
checkbox.click();
Assert.ok(
- !checkbox.checked,
- "checkbox should be unchecked after clicking on checkbox"
+ checkbox.checked,
+ "checkbox should be checked after clicking on checkbox"
);
- Assert.ok(
- !Services.prefs.getIntPref("accessibility.tabfocus", undefined),
- "No pref value should exist"
+ Assert.equal(
+ Services.prefs.getIntPref("accessibility.tabfocus"),
+ 7,
+ "Should restore default value"
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
- SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 4]] });
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 4]] });
await launchPreferences();
checkbox = gBrowser.contentDocument.querySelector(
"#useFullKeyboardNavigation"
@@ -57,20 +59,20 @@ add_task(async function () {
"checkbox should stay unchecked after setting non-7 pref value"
);
Assert.equal(
- Services.prefs.getIntPref("accessibility.tabfocus", 0),
+ Services.prefs.getIntPref("accessibility.tabfocus"),
4,
"pref should have value in store"
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
- SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
+ await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
await launchPreferences();
checkbox = gBrowser.contentDocument.querySelector(
"#useFullKeyboardNavigation"
);
Assert.equal(
- Services.prefs.getIntPref("accessibility.tabfocus", 0),
+ Services.prefs.getIntPref("accessibility.tabfocus"),
7,
"Pref value should update after modification"
);
diff --git a/browser/components/preferences/tests/browser_primaryPassword.js b/browser/components/preferences/tests/browser_primaryPassword.js
index 1162ca1290..2c42bd8d91 100644
--- a/browser/components/preferences/tests/browser_primaryPassword.js
+++ b/browser/components/preferences/tests/browser_primaryPassword.js
@@ -30,6 +30,9 @@ add_task(async function () {
isPrimaryPasswordSet() {
return primaryPasswordSet;
},
+ getOSAuthEnabled() {
+ return true; // Since enabled by default.
+ },
};
let checkbox = doc.querySelector("#useMasterPassword");
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_search_quickactions.js b/browser/components/preferences/tests/browser_search_quickactions.js
index db938035ae..70799b5002 100644
--- a/browser/components/preferences/tests/browser_search_quickactions.js
+++ b/browser/components/preferences/tests/browser_search_quickactions.js
@@ -6,26 +6,26 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
- UrlbarProviderQuickActions:
- "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+ ActionsProviderQuickActions:
+ "resource:///modules/ActionsProviderQuickActions.sys.mjs",
UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
});
add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [
- ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.secondaryActions.featureGate", true],
["browser.urlbar.quickactions.enabled", true],
],
});
- UrlbarProviderQuickActions.addAction("testaction", {
+ ActionsProviderQuickActions.addAction("testaction", {
commands: ["testaction"],
label: "quickactions-downloads2",
});
registerCleanupFunction(() => {
- UrlbarProviderQuickActions.removeAction("testaction");
+ ActionsProviderQuickActions.removeAction("testaction");
});
});
@@ -61,15 +61,23 @@ add_task(async function test_show_prefs() {
await BrowserTestUtils.removeTab(tab);
});
-async function testActionIsShown(window) {
+async function testActionIsShown(window, name) {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "testact",
waitForFocus: SimpleTest.waitForFocus,
});
try {
- let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
- return result.providerName == "quickactions";
+ await BrowserTestUtils.waitForMutationCondition(
+ window.document,
+ {},
+ () =>
+ !!window.document.querySelector(
+ `.urlbarView-action-btn[data-action=${name}]`
+ )
+ );
+ Assert.ok(true, `We found action "${name}"`);
+ return true;
} catch (e) {
return false;
}
@@ -100,7 +108,7 @@ add_task(async function test_prefs() {
});
Assert.ok(
- await testActionIsShown(window),
+ await testActionIsShown(window, "testaction"),
"Actions are shown after user clicks checkbox"
);
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/ResetPBMPanel.sys.mjs b/browser/components/privatebrowsing/ResetPBMPanel.sys.mjs
index f5e818c2a8..51bba1e6af 100644
--- a/browser/components/privatebrowsing/ResetPBMPanel.sys.mjs
+++ b/browser/components/privatebrowsing/ResetPBMPanel.sys.mjs
@@ -45,6 +45,9 @@ export const ResetPBMPanel = {
onViewShowing(aEvent) {
ResetPBMPanel.onViewShowing(aEvent);
},
+ onViewHiding(aEvent) {
+ ResetPBMPanel.onViewHiding(aEvent);
+ },
};
if (this._enabled) {
@@ -59,7 +62,8 @@ export const ResetPBMPanel = {
* the toolbar button.
*/
async onViewShowing(event) {
- let triggeringWindow = event.target.ownerGlobal;
+ let panelview = event.target;
+ let triggeringWindow = panelview.ownerGlobal;
// We may skip the confirmation panel if disabled via pref.
if (!this._shouldConfirmClear) {
@@ -68,7 +72,7 @@ export const ResetPBMPanel = {
// If the action is triggered from the overflow menu make sure that the
// panel gets hidden.
- lazy.CustomizableUI.hidePanelForNode(event.target);
+ lazy.CustomizableUI.hidePanelForNode(panelview);
// Trigger the restart action.
await this._restartPBM(triggeringWindow);
@@ -77,6 +81,8 @@ export const ResetPBMPanel = {
return;
}
+ panelview.addEventListener("command", this);
+
// Before the panel is shown, update checkbox state based on pref.
this._rememberCheck(triggeringWindow).checked = this._shouldConfirmClear;
@@ -86,6 +92,23 @@ export const ResetPBMPanel = {
});
},
+ onViewHiding(event) {
+ let panelview = event.target;
+ panelview.removeEventListener("command", this);
+ },
+
+ handleEvent(event) {
+ let button = event.target;
+ switch (button.id) {
+ case "reset-pbm-panel-cancel-button":
+ this.onCancel(button);
+ break;
+ case "reset-pbm-panel-confirm-button":
+ this.onConfirm(button);
+ break;
+ }
+ },
+
/**
* Handles the confirmation panel cancel button.
* @param {MozButton} button - Cancel button that triggered the action.
@@ -190,7 +213,7 @@ export const ResetPBMPanel = {
});
// In the remaining PBM window: If the sidebar is open close it.
- triggeringWindow.SidebarUI?.hide();
+ triggeringWindow.SidebarController?.hide();
// Clear session store data for the remaining PBM window.
lazy.SessionStore.purgeDataForPrivateWindow(triggeringWindow);
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_default_pin_promo.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_pin_promo.js
index bc62556b12..d34b93f4de 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_pin_promo.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_about_default_pin_promo.js
@@ -54,57 +54,3 @@ add_task(async function test_pin_promo() {
await BrowserTestUtils.closeWindow(win3);
await BrowserTestUtils.closeWindow(win4);
});
-
-add_task(async function test_pin_promo_mr2022_holdback() {
- ASRouter.resetMessageState();
- // Set majorRelease2022 feature onboarding variable fallback pref
- // for inMr2022Holdback targeting to evaluate true
- await SpecialPowers.pushPrefEnv({
- set: [["browser.majorrelease.onboarding", false]],
- });
- await ASRouter.onPrefChange();
- let { win: win1, tab: tab1 } = await openTabAndWaitForRender();
-
- await SpecialPowers.spawn(tab1, [], async function () {
- const promoContainer = content.document.querySelector(".promo");
- const promoButton = content.document.querySelector(
- "#private-browsing-promo-link"
- );
-
- ok(promoContainer, "Promo is shown");
-
- Assert.equal(
- promoButton.getAttribute("data-l10n-id"),
- "about-private-browsing-focus-promo-cta",
- "Pin Promo not shown for holdback user"
- );
- });
-
- await BrowserTestUtils.closeWindow(win1);
-});
-
-add_task(async function test_pin_promo_mr2022_not_holdback() {
- ASRouter.resetMessageState();
- // Set majorRelease2022 feature onboarding variable fallback pref
- // for inMr2022Holdback targeting to evaluate false
- await SpecialPowers.pushPrefEnv({
- set: [["browser.majorrelease.onboarding", true]],
- });
- await ASRouter.onPrefChange();
- let { win: win1, tab: tab1 } = await openTabAndWaitForRender();
-
- await SpecialPowers.spawn(tab1, [], async function () {
- const promoContainer = content.document.querySelector(".promo");
- const promoHeader = content.document.getElementById("promo-header");
-
- ok(promoContainer, "Promo is shown");
-
- is(
- promoHeader.getAttribute("data-l10n-id"),
- "about-private-browsing-pin-promo-header",
- "Pin Promo is shown"
- );
- });
-
- await BrowserTestUtils.closeWindow(win1);
-});
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_resetPBM.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_resetPBM.js
index a80d818c87..f443b9bcd9 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_resetPBM.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_resetPBM.js
@@ -682,23 +682,23 @@ add_task(async function test_reset_action_closes_sidebar() {
info(
"Open the sidebar of both the private browsing window and the normal browsing window."
);
- await SidebarUI.show("viewBookmarksSidebar");
- await win.SidebarUI.show("viewBookmarksSidebar");
+ await SidebarController.show("viewBookmarksSidebar");
+ await win.SidebarController.show("viewBookmarksSidebar");
info("Trigger the restart PBM action");
await ResetPBMPanel._restartPBM(win);
Assert.ok(
- SidebarUI.isOpen,
+ SidebarController.isOpen,
"Normal browsing window sidebar should still be open."
);
Assert.ok(
- !win.SidebarUI.isOpen,
+ !win.SidebarController.isOpen,
"Private browsing sidebar should be closed."
);
// Cleanup: Close the sidebar of the normal browsing window.
- SidebarUI.hide();
+ SidebarController.hide();
// Cleanup: Close the private window that remained open.
await BrowserTestUtils.closeWindow(win);
diff --git a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js
index 58a333bfdb..9988f35969 100644
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_sidebar.js
@@ -12,7 +12,7 @@ function test() {
// opens a sidebar
function openSidebar(win) {
- return win.SidebarUI.show("viewBookmarksSidebar").then(() => win);
+ return win.SidebarController.show("viewBookmarksSidebar").then(() => win);
}
let windowCache = [];
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.html b/browser/components/protections/content/protections.html
index 1374c30fd7..4f5b2d71a9 100644
--- a/browser/components/protections/content/protections.html
+++ b/browser/components/protections/content/protections.html
@@ -13,7 +13,6 @@
<meta name="color-scheme" content="light dark" />
<link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="browser/protections.ftl" />
- <link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<!-- Temporary "en-US"-only l10n strings -->
<link rel="localization" href="preview/protections.ftl" />
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.toml b/browser/components/resistfingerprinting/test/browser/browser.toml
index 8fbd7b5b2f..dd60e383cb 100644
--- a/browser/components/resistfingerprinting/test/browser/browser.toml
+++ b/browser/components/resistfingerprinting/test/browser/browser.toml
@@ -10,6 +10,19 @@ support-files = [
"file_workerNetInfo.js",
"file_workerPerformance.js",
"head.js",
+ "file_canvascompare_aboutblank_iframee.html",
+ "file_canvascompare_aboutblank_iframer.html",
+ "file_canvascompare_aboutblank_popupmaker.html",
+ "file_canvascompare_blob_iframee.html",
+ "file_canvascompare_blob_iframer.html",
+ "file_canvascompare_blob_popupmaker.html",
+ "file_canvascompare_data_iframee.html",
+ "file_canvascompare_data_iframer.html",
+ "file_canvascompare_data_popupmaker.html",
+ "file_canvascompare_iframer.html",
+ "file_canvascompare_iframee.html",
+ "file_canvas_iframer.html",
+ "file_canvas_iframee.html",
"file_navigator_header.sjs",
"file_navigator_iframer.html",
"file_navigator_iframee.html",
@@ -41,17 +54,48 @@ support-files = [
]
["browser_animationapi_iframes.js"]
+lineno = "48"
["browser_block_mozAddonManager.js"]
+lineno = "51"
["browser_bug1369357_site_specific_zoom_level.js"]
https_first_disabled = true
+lineno = "54"
+
+["browser_canvas_iframes.js"]
+lineno = "58"
+
+["browser_canvas_popups.js"]
+lineno = "61"
+
+["browser_canvascompare_iframes.js"]
+lineno = "64"
+
+["browser_canvascompare_iframes_aboutblank.js"]
+
+["browser_canvascompare_iframes_blob.js"]
+
+["browser_canvascompare_iframes_data.js"]
+
+["browser_canvascompare_popups.js"]
+lineno = "66"
+
+["browser_canvascompare_popups_aboutblank.js"]
+lineno = "68"
+
+["browser_canvascompare_popups_blob.js"]
+
+["browser_canvascompare_popups_data.js"]
["browser_cross_origin_isolated_animation_api.js"]
+lineno = "70"
["browser_cross_origin_isolated_performance_api.js"]
+lineno = "73"
["browser_cross_origin_isolated_reduce_time_precision.js"]
+lineno = "76"
["browser_dynamical_window_rounding.js"]
https_first_disabled = true
@@ -59,66 +103,96 @@ skip-if = [
"os == 'mac'", # Bug 1570812
"os == 'linux'", # Bug 1570812, Bug 1775698
]
+lineno = "79"
["browser_hwconcurrency_etp_iframes.js"]
+lineno = "87"
["browser_hwconcurrency_iframes.js"]
+lineno = "90"
["browser_hwconcurrency_iframes_aboutblank.js"]
+lineno = "93"
["browser_hwconcurrency_iframes_aboutsrcdoc.js"]
+lineno = "96"
["browser_hwconcurrency_iframes_blob.js"]
+lineno = "99"
["browser_hwconcurrency_iframes_blobcrossorigin.js"]
+lineno = "102"
["browser_hwconcurrency_iframes_data.js"]
+lineno = "105"
["browser_hwconcurrency_iframes_sandboxediframe.js"]
+lineno = "108"
["browser_hwconcurrency_popups.js"]
+lineno = "111"
["browser_hwconcurrency_popups_aboutblank.js"]
+lineno = "114"
["browser_hwconcurrency_popups_blob.js"]
+lineno = "117"
["browser_hwconcurrency_popups_blob_noopener.js"]
+lineno = "120"
["browser_hwconcurrency_popups_data.js"]
+lineno = "123"
["browser_hwconcurrency_popups_data_noopener.js"]
+lineno = "126"
["browser_hwconcurrency_popups_noopener.js"]
+lineno = "129"
["browser_math.js"]
+lineno = "132"
["browser_navigator.js"]
https_first_disabled = true
+lineno = "135"
["browser_navigator_iframes.js"]
https_first_disabled = true
+lineno = "139"
["browser_netInfo.js"]
https_first_disabled = true
+lineno = "143"
["browser_performanceAPI.js"]
+lineno = "147"
["browser_performanceAPIWorkers.js"]
+lineno = "150"
["browser_reduceTimePrecision_iframes.js"]
https_first_disabled = true
+lineno = "153"
["browser_roundedWindow_dialogWindow.js"]
+lineno = "157"
["browser_roundedWindow_newWindow.js"]
+lineno = "160"
["browser_roundedWindow_open_max_inner.js"]
+lineno = "163"
["browser_roundedWindow_open_mid_inner.js"]
+lineno = "166"
["browser_roundedWindow_open_min_inner.js"]
+lineno = "169"
["browser_spoofing_keyboard_event.js"]
skip-if = ["(debug || asan) && os == 'linux' && bits == 64"] #Bug 1518179
+lineno = "172"
["browser_timezone.js"]
+lineno = "176"
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvas_iframes.js b/browser/components/resistfingerprinting/test/browser/browser_canvas_iframes.js
new file mode 100644
index 0000000000..2b6497cc85
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvas_iframes.js
@@ -0,0 +1,217 @@
+/**
+ * This tests that the canvas is correctly randomized on the iframe (not the framer)
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+ *
+ * - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
+ * - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain
+ * - (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee
+ * - (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain
+ * - (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain
+ * - (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain
+ * - (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain
+ * - (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+ let differences = countDifferencesInUint8Arrays(
+ result,
+ UNMODIFIED_CANVAS_DATA
+ );
+
+ Assert.greaterOrEqual(
+ differences,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization - did not see enough random pixels.`
+ );
+ Assert.lessOrEqual(
+ differences,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization - saw too many random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Be sure to always use `let expectedResults = structuredClone(rfpFullyRandomized)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+const rfpFullyRandomized = [10000, 999999999];
+const fppRandomized = [1, 260];
+const noRandom = [0, 0];
+
+// Note that the starting page and the iframe will be cross-domain from each other, but this test does not check that we inherit the randomizationkey,
+// only that the iframe is randomized.
+const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvas_iframer.html?mode=iframe`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
+expectedResults = structuredClone(noRandom);
+add_task(testA.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain
+expectedResults = structuredClone(noRandom);
+add_task(testB.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (C) RFP is exempted on the framer and (if needed) on another cross-origin domain, but not the framee
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(testC.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (D) RFP is exempted on the framer but not the framee nor another (if needed) cross-origin domain
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(testD.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (E) RFP is not exempted on the framer nor the framee but (if needed) is exempted on another cross-origin domain
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(testE.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (F) RFP is not exempted on the framer nor the framee nor another (if needed) cross-origin domain
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(testF.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (G) RFP is not exempted on the framer but is on the framee and (if needed) on another cross-origin domain
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(testG.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (H) RFP is not exempted on the framer nor another (if needed) cross-origin domain but is on the framee
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(testH.bind(null, uri, testCanvasRandomization, expectedResults));
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvas_popups.js b/browser/components/resistfingerprinting/test/browser/browser_canvas_popups.js
new file mode 100644
index 0000000000..234529b988
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvas_popups.js
@@ -0,0 +1,198 @@
+/**
+ * This tests that the canvas is correctly randomized on a popup (not the starting page)
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+
+ *
+ * - (A) RFP is exempted on the maker and popup
+ * - (C) RFP is exempted on the maker but not the popup
+ * - (E) RFP is not exempted on the maker nor the popup
+ * - (G) RFP is not exempted on the maker but is on the popup
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+ let differences = countDifferencesInUint8Arrays(
+ result,
+ UNMODIFIED_CANVAS_DATA
+ );
+
+ Assert.greaterOrEqual(
+ differences,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization - did not see enough random pixels.`
+ );
+ Assert.lessOrEqual(
+ differences,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization - saw too many random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Be sure to always use `let expectedResults = structuredClone(rfpFullyRandomized)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+const rfpFullyRandomized = [10000, 999999999];
+const fppRandomized = [1, 260];
+const noRandom = [0, 0];
+
+// Note that the starting page and the popup will be cross-domain from each other, but this test does not check that we inherit the randomizationkey,
+// only that the popup is randomized.
+const uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvas_iframer.html?mode=popup`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// (A) RFP is exempted on the opener and openee
+expectedResults = structuredClone(noRandom);
+add_task(testA.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (C) RFP is exempted on the opener but not the openee
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(testC.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (E) RFP is not exempted on the opener nor the openee
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(testE.bind(null, uri, testCanvasRandomization, expectedResults));
+
+// (G) RFP is not exempted on the opener but is on the openee
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(testG.bind(null, uri, testCanvasRandomization, expectedResults));
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes.js b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes.js
new file mode 100644
index 0000000000..a4da3aa9ec
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes.js
@@ -0,0 +1,263 @@
+/**
+ * This test compares canvas randomization on a parent and an iframe, and ensures that the canvas randomization key
+ * is inherited correctly. (e.g. that the canvases have the same random value)
+ *
+ * It runs all the tests twice - once for when the iframe is cross-domain, and once when it is same-domain
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+
+ let parent = result.mine;
+ let child = result.theirs;
+
+ let differencesInRandom = countDifferencesInUint8Arrays(parent, child);
+ let differencesFromUnmodifiedParent = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ parent
+ );
+ let differencesFromUnmodifiedChild = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ child
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing parent - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing parent - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing child - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing child - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesInRandom,
+ expectedResults[2],
+ `Checking ${testDesc} and comparing randomization - lower bound for different random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesInRandom,
+ expectedResults[3],
+ `Checking ${testDesc} and comparing randomization - upper bound for different random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+// The third, the minimum number of differences between the canvases of the parent and child
+// The fourth, the maximum number of differences between the canvases of the parent and child
+const rfpFullyRandomized = [10000, 999999999, 20000, 999999999];
+const fppRandomized = [1, 260, 0, 0];
+const noRandom = [0, 0, 0, 0];
+
+// Note that we are inheriting the randomization key ACROSS top-level domains that are cross-domain, because the iframe is a 3rd party domain
+let uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframer.html?mode=iframe`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// And here the we are inheriting the randomization key into an iframe that is same-domain to the parent
+uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframer.html?mode=iframe`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomized);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_aboutblank.js b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_aboutblank.js
new file mode 100644
index 0000000000..1fd9ac2150
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_aboutblank.js
@@ -0,0 +1,266 @@
+/**
+ * This test compares canvas randomization on an iframe and an iframe of about:blank within that iframe, and
+ * ensures that the canvas randomization key is inherited correctly. (e.g. that the canvases have the same
+ * random value.) There's three pages at play here: the parent frame, the iframe, and the about:blank iframe
+ * within the iframe. We only compare the inner-most two, we don't measure the outer one.
+ *
+ * It runs all the tests twice - once for when the iframe is cross-domain from the parent, and once when it is
+ * same-domain. But in both cases the about:blank iframe is same-domain to its parent.
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+
+ let parent = result.mine;
+ let child = result.theirs;
+
+ let differencesInRandom = countDifferencesInUint8Arrays(parent, child);
+ let differencesFromUnmodifiedParent = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ parent
+ );
+ let differencesFromUnmodifiedChild = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ child
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing parent - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing parent - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing child - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing child - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesInRandom,
+ expectedResults[2],
+ `Checking ${testDesc} and comparing randomization - lower bound for different random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesInRandom,
+ expectedResults[3],
+ `Checking ${testDesc} and comparing randomization - upper bound for different random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+// The third, the minimum number of differences between the canvases of the parent and child
+// The fourth, the maximum number of differences between the canvases of the parent and child
+const rfpFullyRandomized = [10000, 999999999, 20000, 999999999];
+const fppRandomizedSameDomain = [1, 260, 0, 0];
+const noRandom = [0, 0, 0, 0];
+
+// Note that we are inheriting the randomization key ACROSS top-level domains that are cross-domain, because the iframe is a 3rd party domain
+let uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframer.html`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// And here the we are inheriting the randomization key into an iframe that is same-domain to the parent
+uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframer.html`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_blob.js b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_blob.js
new file mode 100644
index 0000000000..be7aedb174
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_blob.js
@@ -0,0 +1,266 @@
+/**
+ * This test compares canvas randomization on an iframe and a blob: iframe within that iframe, and
+ * ensures that the canvas randomization key is inherited correctly. (e.g. that the canvases have the same
+ * random value.) There's three pages at play here: the parent frame, the iframe, and the blob iframe
+ * within the iframe. We only compare the inner-most two, we don't measure the outer one.
+ *
+ * It runs all the tests twice - once for when the iframe is cross-domain from the parent, and once when it is
+ * same-domain. But in both cases the blob iframe is same-domain to its parent.
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+
+ let parent = result.mine;
+ let child = result.theirs;
+
+ let differencesInRandom = countDifferencesInUint8Arrays(parent, child);
+ let differencesFromUnmodifiedParent = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ parent
+ );
+ let differencesFromUnmodifiedChild = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ child
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing parent - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing parent - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing child - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing child - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesInRandom,
+ expectedResults[2],
+ `Checking ${testDesc} and comparing randomization - lower bound for different random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesInRandom,
+ expectedResults[3],
+ `Checking ${testDesc} and comparing randomization - upper bound for different random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+// The third, the minimum number of differences between the canvases of the parent and child
+// The fourth, the maximum number of differences between the canvases of the parent and child
+const rfpFullyRandomized = [10000, 999999999, 20000, 999999999];
+const fppRandomizedSameDomain = [1, 260, 0, 0];
+const noRandom = [0, 0, 0, 0];
+
+// Note that we are inheriting the randomization key ACROSS top-level domains that are cross-domain, because the iframe is a 3rd party domain
+let uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframer.html`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// And here the we are inheriting the randomization key into an iframe that is same-domain to the parent
+uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframer.html`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_data.js b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_data.js
new file mode 100644
index 0000000000..dcf10564e0
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_iframes_data.js
@@ -0,0 +1,271 @@
+/**
+ * This test compares canvas randomization on an iframe and a data iframe within that iframe, and
+ * ensures that the canvas randomization key is inherited correctly. (e.g. that the canvases have the same
+ * random value.) There's three pages at play here: the parent frame, the iframe, and the data: iframe
+ * within the iframe. We only compare the inner-most two, we don't measure the outer one.
+ *
+ * It runs all the tests twice - once for when the iframe is cross-domain from the parent, and once when it is
+ * same-domain. But in both cases the data: iframe is same-domain to its parent.
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+
+ let parent = result.mine;
+ let child = result.theirs;
+
+ let differencesInRandom = countDifferencesInUint8Arrays(parent, child);
+ let differencesFromUnmodifiedParent = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ parent
+ );
+ let differencesFromUnmodifiedChild = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ child
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing parent - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing parent - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing child - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing child - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesInRandom,
+ expectedResults[2],
+ `Checking ${testDesc} and comparing randomization - lower bound for different random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesInRandom,
+ expectedResults[3],
+ `Checking ${testDesc} and comparing randomization - upper bound for different random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+// The third, the minimum number of differences between the canvases of the parent and child
+// The fourth, the maximum number of differences between the canvases of the parent and child
+const rfpFullyRandomized = [10000, 999999999, 20000, 999999999];
+const fppRandomizedSameDomain = [1, 260, 0, 0];
+const noRandom = [0, 0, 0, 0];
+
+// Note that we are inheriting the randomization key ACROSS top-level domains that are cross-domain, because the iframe is a 3rd party domain
+let uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframer.html`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// And here the we are inheriting the randomization key into an iframe that is same-domain to the parent
+uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframer.html`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups.js b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups.js
new file mode 100644
index 0000000000..b1b6f5e9d8
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups.js
@@ -0,0 +1,268 @@
+/**
+ * This test compares canvas randomization on a parent and a popup, and ensures that the canvas randomization key
+ * is inherited correctly. (e.g. that the canvases have the same random value)
+ *
+ * It runs all the tests twice - once for when the popup is cross-domain, and once when it is same-domain
+ * We DO NOT inherit a randomization key across cross-domain popups, but we do for same-domain
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+
+ let parent = result.mine;
+ let child = result.theirs;
+
+ let differencesInRandom = countDifferencesInUint8Arrays(parent, child);
+ let differencesFromUnmodifiedParent = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ parent
+ );
+ let differencesFromUnmodifiedChild = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ child
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing parent - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing parent - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing child - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing child - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesInRandom,
+ expectedResults[2],
+ `Checking ${testDesc} and comparing randomization - lower bound for different random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesInRandom,
+ expectedResults[3],
+ `Checking ${testDesc} and comparing randomization - upper bound for different random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// The following are convenience objects that allow you to quickly see what is
+// and is not modified from a logical set of values.
+// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+// The third, the minimum number of differences between the canvases of the parent and child
+// The fourth, the maximum number of differences between the canvases of the parent and child
+const rfpFullyRandomized = [10000, 999999999, 20000, 999999999];
+const fppRandomizedSameDomain = [1, 260, 0, 0];
+const fppRandomizedCrossDomain = [1, 260, 2, 520];
+const noRandom = [0, 0, 0, 0];
+
+// Note that we will be doing two sets of tests - one where the popup is on a cross-domain
+// and one where it is on the same domain. First, we do same domain
+let uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframer.html?mode=popup`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Now, cross-domain.
+uri = `https://${FRAMER_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframer.html?mode=popup`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedCrossDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedCrossDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedCrossDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_aboutblank.js b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_aboutblank.js
new file mode 100644
index 0000000000..5897aba7e5
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_aboutblank.js
@@ -0,0 +1,269 @@
+/**
+ * This test compares canvas randomization on a parent and an about:blank popup, and ensures that the canvas randomization key
+ * is inherited correctly. (e.g. that the canvases have the same random value)
+ *
+ * It runs all the tests twice. Development showed that there were two different code paths that might get taken for
+ * about:blank popup creation depending on when in the page lifecycle the popup is created. I don't understand why
+ * that is, but at time of writing, the subtle difference in the test will hit both code paths.
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+
+ let parent = result.mine;
+ let child = result.theirs;
+
+ let differencesInRandom = countDifferencesInUint8Arrays(parent, child);
+ let differencesFromUnmodifiedParent = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ parent
+ );
+ let differencesFromUnmodifiedChild = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ child
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing parent - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing parent - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing child - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing child - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesInRandom,
+ expectedResults[2],
+ `Checking ${testDesc} and comparing randomization - lower bound for different random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesInRandom,
+ expectedResults[3],
+ `Checking ${testDesc} and comparing randomization - upper bound for different random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// The following are convenience objects that allow you to quickly see what is
+// and is not modified from a logical set of values.
+// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+// The third, the minimum number of differences between the canvases of the parent and child
+// The fourth, the maximum number of differences between the canvases of the parent and child
+const rfpFullyRandomized = [10000, 999999999, 20000, 999999999];
+const fppRandomizedSameDomain = [1, 260, 0, 0];
+const noRandom = [0, 0, 0, 0];
+
+// As detailed in file_canvascompare_aboutblank_popupmaker.html - there is a difference in code
+// paths for propagating information via LoadInfo when the popup is opened during onLoad vs
+// later in the page. The two modes switch between these situations.
+let uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_popupmaker.html?mode=addOnLoadCallback`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Technically mode=addOnLoadCallback adds the one relevant callback; so mode=<anything else> will omit the callback and result in the other scenario
+uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_popupmaker.html?mode=skipOnLoadCallback`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_blob.js b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_blob.js
new file mode 100644
index 0000000000..739aaf07b7
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_blob.js
@@ -0,0 +1,208 @@
+/**
+ * This test compares canvas randomization on a parent and a blob popup, and ensures that the canvas randomization key
+ * is inherited correctly. (e.g. that the canvases have the same random value)
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+
+ let parent = result.mine;
+ let child = result.theirs;
+
+ let differencesInRandom = countDifferencesInUint8Arrays(parent, child);
+ let differencesFromUnmodifiedParent = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ parent
+ );
+ let differencesFromUnmodifiedChild = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ child
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing parent - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing parent - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing child - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing child - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesInRandom,
+ expectedResults[2],
+ `Checking ${testDesc} and comparing randomization - lower bound for different random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesInRandom,
+ expectedResults[3],
+ `Checking ${testDesc} and comparing randomization - upper bound for different random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+// The third, the minimum number of differences between the canvases of the parent and child
+// The fourth, the maximum number of differences between the canvases of the parent and child
+const rfpFullyRandomized = [10000, 999999999, 20000, 999999999];
+const fppRandomizedSameDomain = [1, 260, 0, 0];
+const noRandom = [0, 0, 0, 0];
+
+let uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_popupmaker.html`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_data.js b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_data.js
new file mode 100644
index 0000000000..caca6e0ff9
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/browser_canvascompare_popups_data.js
@@ -0,0 +1,208 @@
+/**
+ * This test compares canvas randomization on a parent and a data popup, and ensures that the canvas randomization key
+ * is inherited correctly. (e.g. that the canvases have the same random value)
+ *
+ * Covers the following cases:
+ * - RFP/FPP is disabled entirely
+ * - RFP is enabled entirely, and only in PBM
+ * - FPP is enabled entirely, and only in PBM
+ * - A normal window when FPP is enabled globally and RFP is enabled in PBM, Protections Enabled and Disabled
+ *
+ */
+
+"use strict";
+
+// =============================================================================================
+
+/**
+ * Compares two Uint8Arrays and returns the number of bits that are different.
+ *
+ * @param {Uint8ClampedArray} arr1 - The first Uint8ClampedArray to compare.
+ * @param {Uint8ClampedArray} arr2 - The second Uint8ClampedArray to compare.
+ * @returns {number} - The number of bits that are different between the two
+ * arrays.
+ */
+function countDifferencesInUint8Arrays(arr1, arr2) {
+ let count = 0;
+ for (let i = 0; i < arr1.length; i++) {
+ let diff = arr1[i] ^ arr2[i];
+ while (diff > 0) {
+ count += diff & 1;
+ diff >>= 1;
+ }
+ }
+ return count;
+}
+
+// =============================================================================================
+
+async function testCanvasRandomization(result, expectedResults, extraData) {
+ let testDesc = extraData.testDesc;
+
+ let parent = result.mine;
+ let child = result.theirs;
+
+ let differencesInRandom = countDifferencesInUint8Arrays(parent, child);
+ let differencesFromUnmodifiedParent = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ parent
+ );
+ let differencesFromUnmodifiedChild = countDifferencesInUint8Arrays(
+ UNMODIFIED_CANVAS_DATA,
+ child
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing parent - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedParent,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing parent - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[0],
+ `Checking ${testDesc} for canvas randomization, comparing child - lower bound for random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesFromUnmodifiedChild,
+ expectedResults[1],
+ `Checking ${testDesc} for canvas randomization, comparing child - upper bound for random pixels.`
+ );
+
+ Assert.greaterOrEqual(
+ differencesInRandom,
+ expectedResults[2],
+ `Checking ${testDesc} and comparing randomization - lower bound for different random pixels.`
+ );
+ Assert.lessOrEqual(
+ differencesInRandom,
+ expectedResults[3],
+ `Checking ${testDesc} and comparing randomization - upper bound for different random pixels.`
+ );
+}
+
+requestLongerTimeout(2);
+
+let expectedResults = {};
+var UNMODIFIED_CANVAS_DATA = undefined;
+
+add_setup(async function () {
+ // Disable the fingerprinting randomization.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.fingerprintingProtection", false],
+ ["privacy.fingerprintingProtection.pbmode", false],
+ ["privacy.resistFingerprinting", false],
+ ],
+ });
+
+ let extractCanvasData = function () {
+ let offscreenCanvas = new OffscreenCanvas(100, 100);
+
+ const context = offscreenCanvas.getContext("2d");
+
+ // Draw a red rectangle
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+ return imageData.data;
+ };
+
+ function runExtractCanvasData(tab) {
+ let code = extractCanvasData.toString();
+ return SpecialPowers.spawn(tab.linkedBrowser, [code], async funccode => {
+ await content.eval(`var extractCanvasData = ${funccode}`);
+ let result = await content.eval(`extractCanvasData()`);
+ return result;
+ });
+ }
+
+ const emptyPage =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ ) + "empty.html";
+
+ // Open a tab for extracting the canvas data.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, emptyPage);
+
+ let data = await runExtractCanvasData(tab);
+ UNMODIFIED_CANVAS_DATA = data;
+
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+// Be sure to always use `let expectedResults = structuredClone(allNotSpoofed)` to do a
+// deep copy and avoiding corrupting the original 'const' object
+// The first value represents the minimum number of random pixels we should see
+// The second, the maximum number of random pixels
+// The third, the minimum number of differences between the canvases of the parent and child
+// The fourth, the maximum number of differences between the canvases of the parent and child
+const rfpFullyRandomized = [10000, 999999999, 20000, 999999999];
+const fppRandomizedSameDomain = [1, 260, 0, 0];
+const noRandom = [0, 0, 0, 0];
+
+let uri = `https://${IFRAME_DOMAIN}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_popupmaker.html`;
+
+expectedResults = structuredClone(noRandom);
+add_task(
+ defaultsTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ defaultsPBMTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simpleRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(rfpFullyRandomized);
+add_task(
+ simplePBMRFPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simpleFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ simplePBMFPPTest.bind(null, uri, testCanvasRandomization, expectedResults)
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, No Protections
+expectedResults = structuredClone(noRandom);
+add_task(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
+
+// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled
+expectedResults = structuredClone(fppRandomizedSameDomain);
+add_task(
+ RFPPBMFPP_NormalMode_ProtectionsTest.bind(
+ null,
+ uri,
+ testCanvasRandomization,
+ expectedResults
+ )
+);
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_hwconcurrency_etp_iframes.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_etp_iframes.js
index 47914e098e..7c66c511a5 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_etp_iframes.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_etp_iframes.js
@@ -91,7 +91,7 @@ add_task(
let extraPrefs = [
["privacy.resistFingerprinting.pbmode", true],
["privacy.fingerprintingProtection", true],
- ["privacy.fingerprintingProtection.overrides", "+HardwareConcurrency"],
+ ["privacy.fingerprintingProtection.overrides", "+NavigatorHWConcurrency"],
];
let this_extraData = structuredClone(extraData);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes.js
index 1b89556f61..e9f7175909 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes.js
@@ -5,7 +5,7 @@
* - RFP is disabled entirely
* - RFP is enabled entirely
* - FPP is enabled entirely
-
+ * - RFP is enabled in PBM, FPP is enabled globally, testing in a Normal Window
*
* - (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
* - (B) RFP is exempted on the framer and framee but is not on another (if needed) cross-origin domain
@@ -60,9 +60,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -95,8 +103,13 @@ add_task(testG.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(testH.bind(null, uri, testHWConcurrency, expectedResults));
-// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
+// Test a Normal Window with RFP Enabled in PBM and FPP enabled in Normal Browsing Mode - but FPP has no No Protections enabled in it (via .overrides pref)
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutblank.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutblank.js
index 1c78cc997f..d48baf3b7d 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutblank.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutblank.js
@@ -60,9 +60,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -98,5 +106,10 @@ add_task(testH.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutsrcdoc.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutsrcdoc.js
index 6a650256a5..fcaba5b5c1 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutsrcdoc.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_aboutsrcdoc.js
@@ -60,9 +60,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -98,5 +106,10 @@ add_task(testH.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blob.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blob.js
index adedb8b96b..9bafe0b43d 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blob.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blob.js
@@ -60,9 +60,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -98,5 +106,10 @@ add_task(testH.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blobcrossorigin.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blobcrossorigin.js
index 1bb7f268a9..be3a55cfb1 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blobcrossorigin.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_blobcrossorigin.js
@@ -63,9 +63,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
// In theory this should be Not Spoofed, however, in this test there is a blob: document that
// has a content principal and a reference to the iframe's parent (when Fission is disabled anyway.)
@@ -112,5 +120,10 @@ add_task(testH.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_data.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_data.js
index 01690bce49..11d4e0ec87 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_data.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_data.js
@@ -60,9 +60,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -98,5 +106,10 @@ add_task(testH.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_sandboxediframe.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_sandboxediframe.js
index 05c2b33feb..f783937501 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_sandboxediframe.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_iframes_sandboxediframe.js
@@ -60,9 +60,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the framer and framee and (if needed) on another cross-origin domain
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -98,5 +106,10 @@ add_task(testH.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups.js
index a9e3591b62..5515da2a7a 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups.js
@@ -56,9 +56,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the maker and popup
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -78,5 +86,10 @@ add_task(testG.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_aboutblank.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_aboutblank.js
index 17f2960b62..7f99d52635 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_aboutblank.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_aboutblank.js
@@ -55,9 +55,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the popup maker
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -69,5 +77,10 @@ add_task(testE.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob.js
index b2f3ebf863..9487df372c 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob.js
@@ -55,9 +55,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the popup maker
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -69,5 +77,10 @@ add_task(testE.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob_noopener.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob_noopener.js
index 06e4166a4d..a4c5871a22 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob_noopener.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_blob_noopener.js
@@ -65,11 +65,35 @@ add_task(
simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData)
);
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(
+ simplePBMRFPTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults,
+ extraData
+ )
+);
+
expectedResults = structuredClone(allSpoofed);
add_task(
simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData)
);
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(
+ simplePBMFPPTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults,
+ extraData
+ )
+);
+
// (A) RFP is exempted on the popup maker
// Ordinarily, RFP would be exempted, however because the opener relationship is severed
// there is nothing to grant it an exemption, so it is not exempted.
@@ -83,7 +107,7 @@ add_task(testE.bind(null, uri, testHWConcurrency, expectedResults, extraData));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
null,
uri,
testHWConcurrency,
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data.js
index 7499c55303..1a9353bbf4 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data.js
@@ -55,9 +55,17 @@ add_task(defaultsTest.bind(null, uri, testHWConcurrency, expectedResults));
expectedResults = structuredClone(allSpoofed);
add_task(simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMRFPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
expectedResults = structuredClone(allSpoofed);
add_task(simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(simplePBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults));
+
// (A) RFP is exempted on the popup maker
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults));
@@ -69,5 +77,10 @@ add_task(testE.bind(null, uri, testHWConcurrency, expectedResults));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(null, uri, testHWConcurrency, expectedResults)
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults
+ )
);
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data_noopener.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data_noopener.js
index 75f79ba10c..3d90fb61ff 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data_noopener.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_data_noopener.js
@@ -65,11 +65,35 @@ add_task(
simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData)
);
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(
+ simplePBMRFPTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults,
+ extraData
+ )
+);
+
expectedResults = structuredClone(allSpoofed);
add_task(
simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData)
);
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(
+ simplePBMFPPTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults,
+ extraData
+ )
+);
+
// (A) RFP is exempted on the popup maker
// Ordinarily, RFP would be exempted, however because the opener relationship is severed
// there is nothing to grant it an exemption, so it is not exempted.
@@ -83,7 +107,7 @@ add_task(testE.bind(null, uri, testHWConcurrency, expectedResults, extraData));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
null,
uri,
testHWConcurrency,
diff --git a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_noopener.js b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_noopener.js
index 96125c5e20..b9160eb245 100644
--- a/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_noopener.js
+++ b/browser/components/resistfingerprinting/test/browser/browser_hwconcurrency_popups_noopener.js
@@ -65,11 +65,35 @@ add_task(
simpleRFPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData)
);
+// Test a private window with RFP enabled in PBMode
+expectedResults = structuredClone(allSpoofed);
+add_task(
+ simplePBMRFPTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults,
+ extraData
+ )
+);
+
expectedResults = structuredClone(allSpoofed);
add_task(
simpleFPPTest.bind(null, uri, testHWConcurrency, expectedResults, extraData)
);
+// Test a Private Window with FPP Enabled in PBM
+expectedResults = structuredClone(allSpoofed);
+add_task(
+ simplePBMFPPTest.bind(
+ null,
+ uri,
+ testHWConcurrency,
+ expectedResults,
+ extraData
+ )
+);
+
// (A) RFP is exempted on the maker and popup
expectedResults = structuredClone(allNotSpoofed);
add_task(testA.bind(null, uri, testHWConcurrency, expectedResults, extraData));
@@ -91,7 +115,7 @@ add_task(testG.bind(null, uri, testHWConcurrency, expectedResults, extraData));
// Test RFP Enabled in PBM and FPP enabled in Normal Browsing Mode
expectedResults = structuredClone(allNotSpoofed);
add_task(
- simpleRFPPBMFPPTest.bind(
+ RFPPBMFPP_NormalMode_NoProtectionsTest.bind(
null,
uri,
testHWConcurrency,
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_canvas_iframee.html b/browser/components/resistfingerprinting/test/browser/file_canvas_iframee.html
new file mode 100644
index 0000000000..811eb7ee46
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvas_iframee.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<script>
+var parent_window;
+let params = new URLSearchParams(document.location.search);
+if (params.get("mode") == "popup") {
+ parent_window = window.opener;
+} else {
+ parent_window = window.parent;
+}
+
+window.onload = async () => {
+ parent_window.postMessage("ready", "*");
+}
+
+window.addEventListener("message", async function listener(event) {
+ if (event.data[0] == "gimme") {
+ let result = give_result();
+ parent_window.postMessage(result, "*")
+ }
+});
+
+function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+}
+</script>
+<output id="result"></output>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvas_iframer.html b/browser/components/resistfingerprinting/test/browser/file_canvas_iframer.html
new file mode 100644
index 0000000000..3c9f2b65a7
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvas_iframer.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title></title>
+<script src="shared_test_funcs.js"></script>
+<script>
+async function runTheTest(iframe_domain, cross_origin_domain) {
+ var child_reference;
+ let url = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_canvas_iframee.html?mode=`
+ let params = new URLSearchParams(document.location.search);
+
+ if (params.get("mode") == 'iframe') {
+ const iframes = document.querySelectorAll("iframe");
+ iframes[0].src = url + 'iframe';
+ child_reference = iframes[0].contentWindow;
+ } else if (params.get("mode") == "popup") {
+ let options = "";
+ if (params.get("submode") == "noopener") {
+ options = "noopener";
+ }
+ const popup = window.open(url + 'popup', '', options);
+ if (params.get("submode") == "noopener") {
+ return {};
+ }
+ child_reference = popup;
+ } else {
+ throw new Error("Unknown page mode specified");
+ }
+
+ await waitForMessage("ready", `https://${iframe_domain}`);
+
+ const promiseForRFPTest = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ if(event.origin != `https://${iframe_domain}`) {
+ throw new Error(`origin should be ${iframe_domain}`);
+ }
+ resolve(event.data);
+ }, { once: true });
+ });
+ child_reference.postMessage(["gimme", cross_origin_domain], "*");
+ var result = await promiseForRFPTest;
+
+ if (params.get("mode") == "popup") {
+ child_reference.close();
+ }
+
+ return result;
+}
+</script>
+</head>
+<body>
+<iframe width=100></iframe>
+</body>
+</html>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframee.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframee.html
new file mode 100644
index 0000000000..c123ecc3e9
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframee.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<body>
+<output id="result"></output>
+<script>
+window.onload = async () => {
+ parent.postMessage("ready", "*");
+}
+
+window.addEventListener("message", async function listener(event) {
+ if (event.data[0] == "gimme") {
+ var iframe = document.createElement("iframe");
+ iframe.src = "about:blank?foo";
+ document.body.append(iframe);
+
+ function test() {
+ function give_inner_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ window.parent.document.querySelector("#result").textContent = JSON.stringify(give_inner_result());
+ }
+
+ iframe.contentWindow.eval(`(${test})()`);
+
+
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ let myResult = give_result();
+
+ parent.postMessage({mine: myResult, theirs: JSON.parse(document.querySelector("#result").textContent)}, "*")
+
+ // Fun fact - without clearing the text content of the element, the test will hang on shutdown
+ // Guess how many hours it took to figure _that_ out?
+ document.querySelector("#result").textContent = '';
+ }
+});
+</script>
+</body>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframer.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframer.html
new file mode 100644
index 0000000000..71a27e6098
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_iframer.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title></title>
+<script src="shared_test_funcs.js"></script>
+<script>
+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_canvascompare_aboutblank_iframee.html`;
+ await waitForMessage("ready", `https://${iframe_domain}`);
+
+ const promiseForRFPTest = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ if(event.origin != `https://${iframe_domain}`) {
+ throw new Error(`origin should be ${iframe_domain}`);
+ }
+ resolve(event.data);
+ }, { once: true });
+ });
+ iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*");
+ var result = await promiseForRFPTest;
+
+ return result;
+}
+</script>
+</head>
+<body>
+<iframe width=100></iframe>
+</body>
+</html>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_popupmaker.html
new file mode 100644
index 0000000000..74e54ffdf2
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_aboutblank_popupmaker.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<script src="shared_test_funcs.js"></script>
+<script>
+var popup = undefined;
+function createPopup() {
+ if(popup === undefined) {
+ let s = `
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+
+ window.addEventListener('message', async function listener(event) {
+ if (event.data[0] == 'popup_is_ready') {
+ window.opener.postMessage(["popup_ready"], "*");
+ } else if (event.data[0] == 'popup_request') {
+ window.opener.postMessage(['popup_response', give_result()], '*');
+ window.close();
+ }
+ });
+ setInterval(function() {
+ if(!window.opener || window.opener.closed) {
+ window.close();
+ }
+ }, 50);`;
+
+ popup = window.open("about:blank", "");
+ popup.eval(s);
+ }
+}
+
+/*
+ * Believe it or not, when the popup is created alters the code paths for
+ * how the RandomKey is populated on the CJS of the popup. It's a pretty
+ * drastic change, and the two changes in the substative (non-test) patch
+ * of this bug are the two different locations. I'll also note that it took
+ * probably 20 hours or more of work to figure out the LoadInfo ctor one,
+ * so I want to have test coverage of both paths, even if I don't understand
+ * _why_ there are two paths.
+ */
+let params = new URLSearchParams(document.location.search);
+if (params.get("mode") == 'addOnLoadCallback') {
+ window.addEventListener("load", createPopup);
+}
+
+async function runTheTest(iframe_domain, cross_origin_domain) {
+ await new Promise(r => setTimeout(r, 2000));
+
+ if (document.readyState !== 'complete') {
+ createPopup();
+ } else if(popup === undefined) {
+ createPopup();
+ }
+
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ let myResult = give_result();
+
+ popup.postMessage(["popup_is_ready", cross_origin_domain], "*");
+ await waitForMessage("popup_ready", `*`);
+
+ const promiseForRFPTest = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ resolve({mine: myResult, theirs: event.data[1]});
+ }, { once: true });
+ });
+
+ popup.postMessage(["popup_request", cross_origin_domain], "*");
+ var result = await promiseForRFPTest;
+
+ popup.close();
+
+ return result;
+}
+
+</script>
+<output id="result"></output>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframee.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframee.html
new file mode 100644
index 0000000000..84e785777e
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframee.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<script type="text/javascript">
+window.onload = async () => {
+ parent.postMessage("ready", "*");
+}
+
+function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+}
+
+window.addEventListener("message", async function listener(event) {
+//window.addEventListener("load", async function listener(event) {
+ if (event.data[0] == "gimme") {
+ // eslint-disable-next-line
+ var s = `<html><body><script>
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ window.parent.document.querySelector('#result').textContent = JSON.stringify(give_result());
+ window.parent.postMessage(["frame_response"], "*");`;
+ // eslint-disable-next-line
+ s += `</` + `script></body></html>`;
+
+ let b = new Blob([s], { type: "text/html" });
+ let url = URL.createObjectURL(b);
+
+ var iframe = document.createElement("iframe");
+ iframe.src = url;
+ document.body.append(iframe);
+ } else if (event.data[0] == "frame_response") {
+ let myResult = give_result();
+ console.log("myResult", myResult)
+
+ let result = JSON.parse(document.querySelector("#result").textContent);
+ console.log("theirResult", result)
+ parent.postMessage({mine: myResult, theirs: result}, "*")
+ }
+});
+</script>
+<body>
+<output id="result" style="display:none"></output>
+</body>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframer.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframer.html
new file mode 100644
index 0000000000..ac64101600
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_iframer.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title></title>
+<script src="shared_test_funcs.js"></script>
+<script>
+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_canvascompare_blob_iframee.html`;
+ await waitForMessage("ready", `https://${iframe_domain}`);
+
+ const promiseForRFPTest = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ if(event.origin != `https://${iframe_domain}`) {
+ throw new Error(`origin should be ${iframe_domain}`);
+ }
+ resolve(event.data);
+ }, { once: true });
+ });
+ iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*");
+ var result = await promiseForRFPTest;
+
+ return result;
+}
+</script>
+</head>
+<body>
+<iframe width=100></iframe>
+</body>
+</html>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_popupmaker.html
new file mode 100644
index 0000000000..454ecb0a7f
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_blob_popupmaker.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<script src="shared_test_funcs.js"></script>
+<script type="text/javascript">
+var popup;
+async function runTheTest(iframe_domain, cross_origin_domain) {
+ let s = `<html><script>
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ window.addEventListener('load', async function listener(event) {
+ window.opener.postMessage(["popup_ready"], "*");
+ });
+ window.addEventListener('message', async function listener(event) {
+ if (event.data[0] == 'popup_request') {
+ window.opener.postMessage(['popup_response', give_result()], '*');
+ window.close();
+ }
+ });`;
+ // eslint-disable-next-line
+ s += `</` + `script></html>`;
+
+ let params = new URLSearchParams(document.location.search);
+ let options = "";
+ if (params.get("submode") == "noopener") {
+ options = "noopener";
+ }
+
+ let b = new Blob([s], { type: "text/html" });
+ let url = URL.createObjectURL(b);
+ popup = window.open(url, "", options);
+
+ if (params.get("submode") == "noopener") {
+ return {};
+ }
+
+ await waitForMessage("popup_ready", `*`);
+
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ let myResult = give_result();
+
+ const promiseForRFPTest = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ resolve({mine: myResult, theirs: event.data[1]});
+ }, { once: true });
+ });
+
+ popup.postMessage(["popup_request", cross_origin_domain], "*");
+ var result = await promiseForRFPTest;
+
+ popup.close();
+
+ return result;
+}
+</script>
+<body>
+<output id="result"></output>
+</body>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframee.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframee.html
new file mode 100644
index 0000000000..856dc8b33d
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframee.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<script type="text/javascript">
+window.onload = async () => {
+ parent.postMessage("ready", "*");
+}
+
+window.addEventListener("message", async function listener(event) {
+ if (event.data[0] == "gimme") {
+ var s = `<html><script>
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ window.addEventListener("load", async function listener(event) {
+ parent.postMessage(["frame_ready"], "*");
+ });
+ window.addEventListener('message', async function listener(event) {
+ if (event.data[0] == 'frame_request') {
+
+ parent.postMessage(['frame_response', give_result()], '*');
+ }
+ });`;
+ // eslint-disable-next-line
+ s += `</` + `script></html>`;
+
+ let iframe = document.createElement("iframe");
+ iframe.src = "data:text/html;base64," + btoa(s);
+ document.body.append(iframe);
+ } else if (event.data[0] == "frame_ready") {
+ let iframe = document.getElementsByTagName("iframe")[0];
+ iframe.contentWindow.postMessage(["frame_request"], "*");
+ } else if (event.data[0] == "frame_response") {
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ let myResult = give_result();
+
+ parent.postMessage({mine: myResult, theirs: event.data[1]}, "*")
+ }
+});
+</script>
+<body>
+<output id="result"></output>
+</body>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframer.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframer.html
new file mode 100644
index 0000000000..c62e5367cb
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_iframer.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title></title>
+<script src="shared_test_funcs.js"></script>
+<script>
+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_canvascompare_data_iframee.html`;
+ await waitForMessage("ready", `https://${iframe_domain}`);
+
+ const promiseForRFPTest = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ if(event.origin != `https://${iframe_domain}`) {
+ throw new Error(`origin should be ${iframe_domain}`);
+ }
+ resolve(event.data);
+ }, { once: true });
+ });
+ iframes[0].contentWindow.postMessage(["gimme", cross_origin_domain], "*");
+ var result = await promiseForRFPTest;
+
+ return result;
+}
+</script>
+</head>
+<body>
+<iframe width=100></iframe>
+</body>
+</html>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_popupmaker.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_popupmaker.html
new file mode 100644
index 0000000000..b9cefeb197
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_data_popupmaker.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<script src="shared_test_funcs.js"></script>
+<script type="text/javascript">
+var popup;
+async function runTheTest(iframe_domain, cross_origin_domain) {
+ let s = `<!DOCTYPE html><html><script>
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ window.addEventListener('load', async function listener(event) {
+ window.opener.postMessage(["popup_ready"], "*");
+ });
+ window.addEventListener('message', async function listener(event) {
+ if (event.data[0] == 'popup_request') {
+ window.opener.postMessage(['popup_response', give_result()], '*');
+ window.close();
+ }
+ });`;
+ // eslint-disable-next-line
+ s += `</` + `script></html>`;
+
+ let params = new URLSearchParams(document.location.search);
+ let options = "";
+ if (params.get("submode") == "noopener") {
+ options = "noopener";
+ }
+
+ let url = "data:text/html;base64," + btoa(s);
+ popup = window.open(url, "", options);
+
+ if (params.get("submode") == "noopener") {
+ return {};
+ }
+
+ await waitForMessage("popup_ready", `*`);
+
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ let myResult = give_result();
+
+ const promiseForRFPTest = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ resolve({mine: myResult, theirs: event.data[1]});
+ }, { once: true });
+ });
+
+ popup.postMessage(["popup_request", cross_origin_domain], "*");
+ var result = await promiseForRFPTest;
+
+ popup.close();
+
+ return result;
+}
+</script>
+<body>
+<output id="result"></output>
+</body>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframee.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframee.html
new file mode 100644
index 0000000000..811eb7ee46
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframee.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<script>
+var parent_window;
+let params = new URLSearchParams(document.location.search);
+if (params.get("mode") == "popup") {
+ parent_window = window.opener;
+} else {
+ parent_window = window.parent;
+}
+
+window.onload = async () => {
+ parent_window.postMessage("ready", "*");
+}
+
+window.addEventListener("message", async function listener(event) {
+ if (event.data[0] == "gimme") {
+ let result = give_result();
+ parent_window.postMessage(result, "*")
+ }
+});
+
+function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+}
+</script>
+<output id="result"></output>
diff --git a/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframer.html b/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframer.html
new file mode 100644
index 0000000000..e164ad21be
--- /dev/null
+++ b/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframer.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title></title>
+<script src="shared_test_funcs.js"></script>
+<script>
+async function runTheTest(iframe_domain, cross_origin_domain) {
+ var child_reference;
+ let url = `https://${iframe_domain}/browser/browser/components/resistfingerprinting/test/browser/file_canvascompare_iframee.html?mode=`
+ let params = new URLSearchParams(document.location.search);
+
+ if (params.get("mode") == 'iframe') {
+ const iframes = document.querySelectorAll("iframe");
+ iframes[0].src = url + 'iframe';
+ child_reference = iframes[0].contentWindow;
+ } else if (params.get("mode") == "popup") {
+ let options = "";
+ if (params.get("submode") == "noopener") {
+ options = "noopener";
+ }
+ const popup = window.open(url + 'popup', '', options);
+ if (params.get("submode") == "noopener") {
+ return {};
+ }
+ child_reference = popup;
+ } else {
+ throw new Error("Unknown page mode specified");
+ }
+
+ function give_result() {
+ const canvas = document.createElement("canvas");
+ canvas.width = 100;
+ canvas.height = 100;
+
+ const context = canvas.getContext("2d");
+
+ context.fillStyle = "#EE2222";
+ context.fillRect(0, 0, 100, 100);
+ context.fillStyle = "#2222EE";
+ context.fillRect(20, 20, 100, 100);
+
+ // Add the canvas element to the document
+ document.body.appendChild(canvas);
+
+ const imageData = context.getImageData(0, 0, 100, 100);
+
+ return imageData.data;
+ }
+ let myResult = give_result();
+
+ await waitForMessage("ready", `https://${iframe_domain}`);
+
+ const promiseForRFPTest = new Promise(resolve => {
+ window.addEventListener("message", event => {
+ if(event.origin != `https://${iframe_domain}`) {
+ throw new Error(`origin should be ${iframe_domain}`);
+ }
+
+ resolve({mine: myResult, theirs: event.data});
+ }, { once: true });
+ });
+ child_reference.postMessage(["gimme", cross_origin_domain], "*");
+ var result = await promiseForRFPTest;
+
+ if (params.get("mode") == "popup") {
+ child_reference.close();
+ }
+
+ return result;
+}
+</script>
+</head>
+<body>
+<iframe width=100></iframe>
+</body>
+</html>
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..26e9656398 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 {
@@ -14,9 +14,7 @@ async function runTheTest(iframe_domain, cross_origin_domain, mode) {
window.opener.postMessage(["popup_ready"], "*");
});
window.addEventListener('message', async function listener(event) {
- if (event.data[0] == 'popup_is_ready') {
- window.opener.postMessage(["popup_ready"], "*");
- } else if (event.data[0] == 'popup_request') {
+ if (event.data[0] == 'popup_request') {
let result = give_result();
window.opener.postMessage(['popup_response', result], '*');
}
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..18a96994c7 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))
);
};
@@ -755,6 +753,30 @@ async function defaultsTest(
}
}
+async function defaultsPBMTest(
+ uri,
+ testFunction,
+ expectedResults,
+ extraData,
+ extraPrefs
+) {
+ if (extraData == undefined) {
+ extraData = {};
+ }
+ extraData.private_window = true;
+ extraData.testDesc = extraData.testDesc || "default PBM window";
+ expectedResults.shouldRFPApply = false;
+ if (extraPrefs != undefined) {
+ await SpecialPowers.pushPrefEnv({
+ set: extraPrefs,
+ });
+ }
+ await runActualTest(uri, testFunction, expectedResults, extraData);
+ if (extraPrefs != undefined) {
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
async function simpleRFPTest(
uri,
testFunction,
@@ -815,7 +837,10 @@ async function simpleFPPTest(
await SpecialPowers.pushPrefEnv({
set: [
["privacy.fingerprintingProtection", true],
- ["privacy.fingerprintingProtection.overrides", "+NavigatorHWConcurrency"],
+ [
+ "privacy.fingerprintingProtection.overrides",
+ "+NavigatorHWConcurrency,+CanvasRandomization",
+ ],
].concat(extraPrefs || []),
});
@@ -840,7 +865,10 @@ async function simplePBMFPPTest(
await SpecialPowers.pushPrefEnv({
set: [
["privacy.fingerprintingProtection.pbmode", true],
- ["privacy.fingerprintingProtection.overrides", "+HardwareConcurrency"],
+ [
+ "privacy.fingerprintingProtection.overrides",
+ "+NavigatorHWConcurrency,+CanvasRandomization",
+ ],
].concat(extraPrefs || []),
});
@@ -849,7 +877,7 @@ async function simplePBMFPPTest(
await SpecialPowers.popPrefEnv();
}
-async function simpleRFPPBMFPPTest(
+async function RFPPBMFPP_NormalMode_NoProtectionsTest(
uri,
testFunction,
expectedResults,
@@ -862,14 +890,49 @@ async function simpleRFPPBMFPPTest(
extraData.private_window = false;
extraData.testDesc =
extraData.testDesc ||
- "RFP Enabled in PBM and FPP enabled in Normal Browsing Mode";
+ "RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Disabled";
expectedResults.shouldRFPApply = false;
await SpecialPowers.pushPrefEnv({
set: [
["privacy.resistFingerprinting", false],
["privacy.resistFingerprinting.pbmode", true],
["privacy.fingerprintingProtection", true],
- ["privacy.fingerprintingProtection.overrides", "-HardwareConcurrency"],
+ [
+ "privacy.fingerprintingProtection.overrides",
+ "-NavigatorHWConcurrency,-CanvasRandomization",
+ ],
+ ].concat(extraPrefs || []),
+ });
+
+ await runActualTest(uri, testFunction, expectedResults, extraData);
+
+ await SpecialPowers.popPrefEnv();
+}
+
+async function RFPPBMFPP_NormalMode_ProtectionsTest(
+ uri,
+ testFunction,
+ expectedResults,
+ extraData,
+ extraPrefs
+) {
+ if (extraData == undefined) {
+ extraData = {};
+ }
+ extraData.private_window = false;
+ extraData.testDesc =
+ extraData.testDesc ||
+ "RFP Enabled in PBM and FPP enabled in Normal Browsing Mode, Protections Enabled";
+ expectedResults.shouldRFPApply = false;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", false],
+ ["privacy.resistFingerprinting.pbmode", true],
+ ["privacy.fingerprintingProtection", true],
+ [
+ "privacy.fingerprintingProtection.overrides",
+ "+NavigatorHWConcurrency,+CanvasRandomization",
+ ],
].concat(extraPrefs || []),
});
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..aa9dbfdbd3 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,29 @@ 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,
+ downloadAttributes,
+ copyAttributes,
+ ] = lazy.overlayLocalization.formatMessagesSync([
+ { id: "screenshots-cancel-button" },
+ { id: "screenshots-component-cancel-button" },
+ { id: "screenshots-instructions" },
+ {
+ id: "screenshots-component-download-button-2",
+ args: { shortcut: downloadShortcut },
+ },
+ {
+ id: "screenshots-component-copy-button-2",
+ args: { shortcut: copyShorcut },
+ },
+ ]);
return `
<template>
@@ -98,7 +115,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>
@@ -116,31 +133,31 @@ export class ScreenshotsOverlay {
<div id="mover-topRight" class="mover-target direction-topRight" tabindex="0">
<div class="mover"></div>
</div>
- <div id="mover-left" class="mover-target direction-left">
- <div class="mover"></div>
- </div>
<div id="mover-right" class="mover-target direction-right">
<div class="mover"></div>
</div>
- <div id="mover-bottomLeft" class="mover-target direction-bottomLeft" tabindex="0">
+ <div id="mover-bottomRight" class="mover-target direction-bottomRight" tabindex="0">
<div class="mover"></div>
</div>
<div id="mover-bottom" class="mover-target direction-bottom">
<div class="mover"></div>
</div>
- <div id="mover-bottomRight" class="mover-target direction-bottomRight" tabindex="0">
+ <div id="mover-bottomLeft" class="mover-target direction-bottomLeft" tabindex="0">
+ <div class="mover"></div>
+ </div>
+ <div id="mover-left" class="mover-target direction-left">
<div class="mover"></div>
</div>
<div id="selection-size-container">
- <span id="selection-size"></span>
+ <span id="selection-size" dir="ltr"></span>
</div>
</div>
</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>${copyAttributes.value}</label></button>
+ <button id="download" class="screenshots-button primary" title="${downloadAttributes.attributes[0].value}" aria-label="${downloadAttributes.attributes[1].value}"><img/><label>${downloadAttributes.value}</label></button>
</div>
</div>
</div>
@@ -180,6 +197,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,10 +229,19 @@ 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);
+ this.selection = this.window.getSelection();
+ this.ranges = [];
+ for (let i = 0; i < this.selection.rangeCount; i++) {
+ this.ranges.push(this.selection.getRangeAt(i));
+ }
+
this.#initialized = true;
}
@@ -271,6 +305,10 @@ export class ScreenshotsOverlay {
};
}
+ focus() {
+ this.previewCancelButton.focus({ focusVisible: true });
+ }
+
/**
* Returns the x and y coordinates of the event relative to both the
* viewport and the page.
@@ -284,16 +322,14 @@ export class ScreenshotsOverlay {
* }
*/
getCoordinatesFromEvent(event) {
- const { clientX, clientY, pageX, pageY } = event;
+ let { clientX, clientY, pageX, pageY } = event;
+ pageX -= this.windowDimensions.scrollMinX;
+ pageY -= this.windowDimensions.scrollMinY;
return { clientX, clientY, pageX, pageY };
}
handleEvent(event) {
- if (event.button > 0) {
- return;
- }
-
switch (event.type) {
case "click":
this.handleClick(event);
@@ -313,24 +349,52 @@ export class ScreenshotsOverlay {
case "keyup":
this.handleKeyUp(event);
break;
+ case "selectionchange":
+ this.handleSelectionChange();
+ break;
}
}
+ /**
+ * 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 +415,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 +453,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);
@@ -431,6 +509,127 @@ export class ScreenshotsOverlay {
* @param {Event} event The keydown event
*/
handleKeyDown(event) {
+ if (event.key === "Escape") {
+ this.maybeCancelScreenshots();
+ return;
+ }
+
+ switch (this.#state) {
+ case STATES.CROSSHAIRS:
+ this.crosshairsKeyDown(event);
+ break;
+ case STATES.DRAGGING:
+ this.draggingKeyDown(event);
+ break;
+ case STATES.RESIZING:
+ this.resizingKeyDown(event);
+ break;
+ case STATES.SELECTED:
+ this.selectedKeyDown(event);
+ break;
+ }
+ }
+
+ /**
+ * Handles when a keyup occurs in the screenshots component.
+ * All we need to do on keyup is set the state to selected.
+ * @param {Event} event The keydown event
+ */
+ handleKeyUp(event) {
+ switch (this.#state) {
+ case STATES.RESIZING:
+ switch (event.key) {
+ case "ArrowLeft":
+ case "ArrowUp":
+ case "ArrowRight":
+ case "ArrowDown":
+ switch (event.originalTarget.id) {
+ case "highlight":
+ case "mover-bottomLeft":
+ case "mover-bottomRight":
+ case "mover-topLeft":
+ case "mover-topRight":
+ event.preventDefault();
+ this.#setState(STATES.SELECTED, { doNotMoveFocus: true });
+ break;
+ }
+ break;
+ }
+ break;
+ }
+ }
+
+ /**
+ * Gets the accel key depending on the platform.
+ * metaKey for macOS. ctrlKey for Windows and Linux.
+ * @param {Event} event The keydown event
+ * @returns {Boolean} True if the accel key is pressed, false otherwise.
+ */
+ getAccelKey(event) {
+ if (AppConstants.platform === "macosx") {
+ return event.metaKey;
+ }
+ return event.ctrlKey;
+ }
+
+ crosshairsKeyDown(event) {
+ switch (event.key) {
+ case "ArrowLeft":
+ case "ArrowUp":
+ case "ArrowRight":
+ case "ArrowDown":
+ // Do nothing so we can prevent default below
+ break;
+ case "Tab":
+ this.maybeLockFocus(event);
+ return;
+ case "Enter":
+ if (this.hoverElementRegion.isRegionValid) {
+ event.preventDefault();
+ this.draggingReadyStart();
+ this.draggingReadyDragEnd();
+ return;
+ }
+ // eslint-disable-next-line no-fallthrough
+ case " ": {
+ if (Services.appinfo.isWayland) {
+ return;
+ }
+
+ if (event.originalTarget === this.previewCancelButton) {
+ return;
+ }
+
+ event.preventDefault();
+ // The left and top coordinates from cursorRegion are relative to
+ // the client window so we need to add the scroll offset of the page to
+ // get the correct coordinates.
+ let x = {};
+ let y = {};
+ this.window.windowUtils.getLastOverWindowPointerLocationInCSSPixels(
+ x,
+ y
+ );
+ this.crosshairsDragStart(
+ x.value + this.windowDimensions.scrollX,
+ y.value + this.windowDimensions.scrollY
+ );
+ this.#setState(STATES.DRAGGING);
+ break;
+ }
+ default:
+ return;
+ }
+
+ // Prevent scrolling with arrow keys
+ event.preventDefault();
+ }
+
+ /**
+ * Handles a keydown event for the dragging state.
+ * @param {Event} event The keydown event
+ */
+ draggingKeyDown(event) {
switch (event.key) {
case "ArrowLeft":
this.handleArrowLeftKeyDown(event);
@@ -444,26 +643,102 @@ export class ScreenshotsOverlay {
case "ArrowDown":
this.handleArrowDownKeyDown(event);
break;
+ case "Enter":
+ case " ":
+ event.preventDefault();
+ this.#setState(STATES.SELECTED);
+ return;
+ default:
+ return;
+ }
+
+ this.drawSelectionContainer();
+ }
+
+ /**
+ * Handles a keydown event for the resizing state.
+ * @param {Event} event The keydown event
+ */
+ resizingKeyDown(event) {
+ switch (event.key) {
+ case "ArrowLeft":
+ this.resizingArrowLeftKeyDown(event);
+ break;
+ case "ArrowUp":
+ this.resizingArrowUpKeyDown(event);
+ break;
+ case "ArrowRight":
+ this.resizingArrowRightKeyDown(event);
+ break;
+ case "ArrowDown":
+ this.resizingArrowDownKeyDown(event);
+ break;
+ }
+ }
+
+ selectedKeyDown(event) {
+ let isSelectionElement = event.originalTarget.closest(
+ "#selection-container"
+ );
+ switch (event.key) {
+ case "ArrowLeft":
+ if (isSelectionElement) {
+ this.resizingArrowLeftKeyDown(event);
+ }
+ break;
+ case "ArrowUp":
+ if (isSelectionElement) {
+ this.resizingArrowUpKeyDown(event);
+ }
+ break;
+ case "ArrowRight":
+ if (isSelectionElement) {
+ this.resizingArrowRightKeyDown(event);
+ }
+ break;
+ case "ArrowDown":
+ if (isSelectionElement) {
+ this.resizingArrowDownKeyDown(event);
+ }
+ break;
case "Tab":
this.maybeLockFocus(event);
break;
- case "Escape":
- this.maybeCancelScreenshots();
+ case " ":
+ if (!event.originalTarget.closest("#buttons-container")) {
+ event.preventDefault();
+ }
+ 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;
}
}
/**
- * Gets the accel key depending on the platform.
- * metaKey for macOS. ctrlKey for Windows and Linux.
+ * Move the region or its left or right side to the left.
+ * Just the arrow key will move the region by 1px.
+ * Arrow key + shift will move the region by 10px.
+ * Arrow key + control/meta will move to the edge of the window.
* @param {Event} event The keydown event
- * @returns {Boolean} True if the accel key is pressed, false otherwise.
*/
- getAccelKey(event) {
- if (AppConstants.platform === "macosx") {
- return event.metaKey;
+ resizingArrowLeftKeyDown(event) {
+ this.handleArrowLeftKeyDown(event);
+
+ if (this.#state !== STATES.RESIZING) {
+ this.#setState(STATES.RESIZING);
}
- return event.ctrlKey;
+
+ this.drawSelectionContainer();
}
/**
@@ -474,6 +749,7 @@ export class ScreenshotsOverlay {
* @param {Event} event The keydown event
*/
handleArrowLeftKeyDown(event) {
+ let exponent = event.shiftKey ? 1 : 0;
switch (event.originalTarget.id) {
case "highlight":
if (this.getAccelKey(event)) {
@@ -483,7 +759,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.right -= 10 ** event.shiftKey;
+ this.selectionRegion.right -= 10 ** exponent;
// eslint-disable-next-line no-fallthrough
case "mover-topLeft":
case "mover-bottomLeft":
@@ -492,7 +768,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.left -= 10 ** event.shiftKey;
+ this.selectionRegion.left -= 10 ** exponent;
this.scrollIfByEdge(
this.selectionRegion.left,
this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
@@ -512,7 +788,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.right -= 10 ** event.shiftKey;
+ this.selectionRegion.right -= 10 ** exponent;
if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
this.selectionRegion.sortCoords();
if (event.originalTarget.id === "mover-topRight") {
@@ -526,11 +802,23 @@ export class ScreenshotsOverlay {
return;
}
+ event.preventDefault();
+ }
+
+ /**
+ * Move the region or its top or bottom side upward.
+ * Just the arrow key will move the region by 1px.
+ * Arrow key + shift will move the region by 10px.
+ * Arrow key + control/meta will move to the edge of the window.
+ * @param {Event} event The keydown event
+ */
+ resizingArrowUpKeyDown(event) {
+ this.handleArrowUpKeyDown(event);
+
if (this.#state !== STATES.RESIZING) {
this.#setState(STATES.RESIZING);
}
- event.preventDefault();
this.drawSelectionContainer();
}
@@ -542,6 +830,7 @@ export class ScreenshotsOverlay {
* @param {Event} event The keydown event
*/
handleArrowUpKeyDown(event) {
+ let exponent = event.shiftKey ? 1 : 0;
switch (event.originalTarget.id) {
case "highlight":
if (this.getAccelKey(event)) {
@@ -551,7 +840,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.bottom -= 10 ** event.shiftKey;
+ this.selectionRegion.bottom -= 10 ** exponent;
// eslint-disable-next-line no-fallthrough
case "mover-topLeft":
case "mover-topRight":
@@ -560,7 +849,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.top -= 10 ** event.shiftKey;
+ this.selectionRegion.top -= 10 ** exponent;
this.scrollIfByEdge(
this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
this.selectionRegion.top
@@ -580,7 +869,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.bottom -= 10 ** event.shiftKey;
+ this.selectionRegion.bottom -= 10 ** exponent;
if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
this.selectionRegion.sortCoords();
if (event.originalTarget.id === "mover-bottomLeft") {
@@ -594,11 +883,23 @@ export class ScreenshotsOverlay {
return;
}
+ event.preventDefault();
+ }
+
+ /**
+ * Move the region or its left or right side to the right.
+ * Just the arrow key will move the region by 1px.
+ * Arrow key + shift will move the region by 10px.
+ * Arrow key + control/meta will move to the edge of the window.
+ * @param {Event} event The keydown event
+ */
+ resizingArrowRightKeyDown(event) {
+ this.handleArrowRightKeyDown(event);
+
if (this.#state !== STATES.RESIZING) {
this.#setState(STATES.RESIZING);
}
- event.preventDefault();
this.drawSelectionContainer();
}
@@ -610,6 +911,7 @@ export class ScreenshotsOverlay {
* @param {Event} event The keydown event
*/
handleArrowRightKeyDown(event) {
+ let exponent = event.shiftKey ? 1 : 0;
switch (event.originalTarget.id) {
case "highlight":
if (this.getAccelKey(event)) {
@@ -620,7 +922,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.left += 10 ** event.shiftKey;
+ this.selectionRegion.left += 10 ** exponent;
// eslint-disable-next-line no-fallthrough
case "mover-topRight":
case "mover-bottomRight":
@@ -630,7 +932,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.right += 10 ** event.shiftKey;
+ this.selectionRegion.right += 10 ** exponent;
this.scrollIfByEdge(
this.selectionRegion.right,
this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
@@ -651,7 +953,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.left += 10 ** event.shiftKey;
+ this.selectionRegion.left += 10 ** exponent;
if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
this.selectionRegion.sortCoords();
if (event.originalTarget.id === "mover-topLeft") {
@@ -665,12 +967,7 @@ export class ScreenshotsOverlay {
return;
}
- if (this.#state !== STATES.RESIZING) {
- this.#setState(STATES.RESIZING);
- }
-
event.preventDefault();
- this.drawSelectionContainer();
}
/**
@@ -680,7 +977,18 @@ export class ScreenshotsOverlay {
* Arrow key + control/meta will move to the edge of the window.
* @param {Event} event The keydown event
*/
+ resizingArrowDownKeyDown(event) {
+ this.handleArrowDownKeyDown(event);
+
+ if (this.#state !== STATES.RESIZING) {
+ this.#setState(STATES.RESIZING);
+ }
+
+ this.drawSelectionContainer();
+ }
+
handleArrowDownKeyDown(event) {
+ let exponent = event.shiftKey ? 1 : 0;
switch (event.originalTarget.id) {
case "highlight":
if (this.getAccelKey(event)) {
@@ -691,7 +999,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.top += 10 ** event.shiftKey;
+ this.selectionRegion.top += 10 ** exponent;
// eslint-disable-next-line no-fallthrough
case "mover-bottomLeft":
case "mover-bottomRight":
@@ -701,7 +1009,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.bottom += 10 ** event.shiftKey;
+ this.selectionRegion.bottom += 10 ** exponent;
this.scrollIfByEdge(
this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
this.selectionRegion.bottom
@@ -722,7 +1030,7 @@ export class ScreenshotsOverlay {
break;
}
- this.selectionRegion.top += 10 ** event.shiftKey;
+ this.selectionRegion.top += 10 ** exponent;
if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
this.selectionRegion.sortCoords();
if (event.originalTarget.id === "mover-topLeft") {
@@ -736,12 +1044,7 @@ export class ScreenshotsOverlay {
return;
}
- if (this.#state !== STATES.RESIZING) {
- this.#setState(STATES.RESIZING);
- }
-
event.preventDefault();
- this.drawSelectionContainer();
}
/**
@@ -750,27 +1053,39 @@ export class ScreenshotsOverlay {
* @param {Event} event The keydown event
*/
maybeLockFocus(event) {
- if (this.#state !== STATES.SELECTED) {
- return;
- }
-
event.preventDefault();
- if (event.originalTarget.id === "highlight" && event.shiftKey) {
- this.downloadButton.focus({ focusVisible: true });
- } else if (event.originalTarget.id === "download" && !event.shiftKey) {
- this.highlightEl.focus({ focusVisible: true });
- } else {
- // The content document can listen for keydown events and prevent moving
- // focus so we manually move focus to the next element here.
- let direction = event.shiftKey
- ? Services.focus.MOVEFOCUS_BACKWARD
- : Services.focus.MOVEFOCUS_FORWARD;
- Services.focus.moveFocus(
- this.window,
- null,
- direction,
- Services.focus.FLAG_BYKEY
- );
+
+ switch (this.#state) {
+ case STATES.CROSSHAIRS:
+ if (event.shiftKey) {
+ this.#dispatchEvent("Screenshots:FocusPanel", {
+ direction: "backward",
+ });
+ } else {
+ this.#dispatchEvent("Screenshots:FocusPanel", {
+ direction: "forward",
+ });
+ }
+ break;
+ case STATES.SELECTED:
+ if (event.originalTarget.id === "highlight" && event.shiftKey) {
+ this.downloadButton.focus({ focusVisible: true });
+ } else if (event.originalTarget.id === "download" && !event.shiftKey) {
+ this.highlightEl.focus({ focusVisible: true });
+ } else {
+ // The content document can listen for keydown events and prevent moving
+ // focus so we manually move focus to the next element here.
+ let direction = event.shiftKey
+ ? Services.focus.MOVEFOCUS_BACKWARD
+ : Services.focus.MOVEFOCUS_FORWARD;
+ Services.focus.moveFocus(
+ this.window,
+ null,
+ direction,
+ Services.focus.FLAG_BYKEY
+ );
+ }
+ break;
}
}
@@ -780,34 +1095,22 @@ 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 });
}
}
/**
- * Handles when a keydown occurs in the screenshots component.
- * All we need to do on keyup is set the state to selected.
- * @param {Event} event The keydown event
+ * All of the selection ranges were recorded at initialization. The ranges
+ * are removed when focus is set to the buttons so we add the selection
+ * ranges back so a selected region can be captured.
*/
- handleKeyUp(event) {
- switch (event.key) {
- case "ArrowLeft":
- case "ArrowUp":
- case "ArrowRight":
- case "ArrowDown":
- switch (event.originalTarget.id) {
- case "highlight":
- case "mover-bottomLeft":
- case "mover-bottomRight":
- case "mover-topLeft":
- case "mover-topRight":
- event.preventDefault();
- this.#setState(STATES.SELECTED);
- break;
- }
- break;
+ handleSelectionChange() {
+ if (this.ranges.length) {
+ for (let range of this.ranges) {
+ this.selection.addRange(range);
+ }
}
}
@@ -829,8 +1132,9 @@ export class ScreenshotsOverlay {
/**
* Set a new state for the overlay
* @param {String} newState
+ * @param {Object} options (optional) Options for calling start of state method
*/
- #setState(newState) {
+ #setState(newState, options = {}) {
if (this.#state === STATES.SELECTED && newState === STATES.CROSSHAIRS) {
this.#dispatchEvent("Screenshots:RecordEvent", {
eventName: "started",
@@ -839,7 +1143,13 @@ export class ScreenshotsOverlay {
}
if (newState !== this.#state) {
this.#dispatchEvent("Screenshots:OverlaySelection", {
- hasSelection: newState == STATES.SELECTED,
+ hasSelection: [
+ STATES.DRAGGING_READY,
+ STATES.DRAGGING,
+ STATES.RESIZING,
+ STATES.SELECTED,
+ ].includes(newState),
+ overlayState: newState,
});
}
this.#state = newState;
@@ -858,7 +1168,7 @@ export class ScreenshotsOverlay {
break;
}
case STATES.SELECTED: {
- this.selectedStart();
+ this.selectedStart(options);
break;
}
case STATES.RESIZING: {
@@ -868,6 +1178,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.
@@ -906,11 +1228,16 @@ export class ScreenshotsOverlay {
* Hide the preview and hover element containers.
* Draw the selection and buttons containers.
*/
- selectedStart() {
+ selectedStart(options) {
+ this.selectionRegion.sortCoords();
this.hidePreviewContainer();
this.hideHoverElementContainer();
this.drawSelectionContainer();
this.drawButtonsContainer();
+
+ if (!options.doNotMoveFocus) {
+ this.setFocusToActionButton();
+ }
}
/**
@@ -1137,7 +1464,6 @@ export class ScreenshotsOverlay {
if (this.hoverElementRegion.isRegionValid) {
this.selectionRegion.dimensions = this.hoverElementRegion.dimensions;
this.#setState(STATES.SELECTED);
- this.setFocusToActionButton();
this.#dispatchEvent("Screenshots:RecordEvent", {
eventName: "selected",
reason: "element",
@@ -1158,11 +1484,9 @@ export class ScreenshotsOverlay {
right: pageX,
bottom: pageY,
};
- this.selectionRegion.sortCoords();
this.#setState(STATES.SELECTED);
this.maybeRecordRegionSelected();
this.#methodsUsed.region += 1;
- this.setFocusToActionButton();
}
/**
@@ -1173,9 +1497,7 @@ export class ScreenshotsOverlay {
*/
resizingDragEnd(pageX, pageY) {
this.resizingDrag(pageX, pageY);
- this.selectionRegion.sortCoords();
this.#setState(STATES.SELECTED);
- this.setFocusToActionButton();
this.maybeRecordRegionSelected();
if (this.#moverId === "highlight") {
this.#methodsUsed.move += 1;
@@ -1234,8 +1556,9 @@ export class ScreenshotsOverlay {
* Update the screenshots overlay container based on the window dimensions.
*/
updateScreenshotsOverlayContainer() {
- let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions;
- this.screenshotsContainer.style = `width:${scrollWidth}px;height:${scrollHeight}px;`;
+ let { scrollWidth, scrollHeight, scrollMinX } =
+ this.windowDimensions.dimensions;
+ this.screenshotsContainer.style = `left:${scrollMinX};width:${scrollWidth}px;height:${scrollHeight}px;`;
}
showScreenshotsOverlayContainer() {
@@ -1285,17 +1608,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-3",
args: {
- width: Math.floor(width * dpr),
- height: Math.floor(height * dpr),
+ width: Math.floor(width * zoom),
+ height: Math.floor(height * zoom),
},
},
]);
@@ -1325,16 +1652,13 @@ export class ScreenshotsOverlay {
right: boxRight,
bottom: boxBottom,
} = this.selectionRegion.dimensions;
- let { clientWidth, clientHeight, scrollX, scrollY } =
+
+ let { clientWidth, clientHeight, scrollX, scrollY, scrollWidth } =
this.windowDimensions.dimensions;
- if (
- boxTop > scrollY + clientHeight ||
- boxBottom < scrollY ||
- boxLeft > scrollX + clientWidth ||
- boxRight < scrollX
- ) {
- // The box is offscreen so need to draw the buttons
+ if (!this.windowDimensions.isInViewport(this.selectionRegion.dimensions)) {
+ // The box is entirely offscreen so need to draw the buttons
+
return;
}
@@ -1350,12 +1674,32 @@ export class ScreenshotsOverlay {
}
}
- if (boxRight < 300) {
- this.buttonsContainer.style.left = `${boxLeft}px`;
- this.buttonsContainer.style.right = "";
- } else {
- this.buttonsContainer.style.right = `calc(100% - ${boxRight}px)`;
+ if (!this.buttonsContainerRect) {
+ this.buttonsContainerRect = this.buttonsContainer.getBoundingClientRect();
+ }
+
+ let viewportLeft = scrollX;
+ let viewportRight = scrollX + clientWidth;
+
+ let left, right;
+ let isLTR = !Services.locale.isAppLocaleRTL;
+ if (isLTR) {
+ left = Math.max(
+ Math.min(viewportRight, boxRight),
+ viewportLeft + Math.ceil(this.buttonsContainerRect.width)
+ );
+ right = scrollWidth - left;
+
+ this.buttonsContainer.style.right = `${right}px`;
this.buttonsContainer.style.left = "";
+ } else {
+ left = Math.min(
+ Math.max(viewportLeft, boxLeft),
+ viewportRight - Math.ceil(this.buttonsContainerRect.width)
+ );
+
+ this.buttonsContainer.style.left = `${left}px`;
+ this.buttonsContainer.style.right = "";
}
this.buttonsContainer.style.top = `${top}px`;
@@ -1369,6 +1713,10 @@ export class ScreenshotsOverlay {
this.buttonsContainer.hidden = true;
}
+ updateCursorRegion(left, top) {
+ this.cursorRegion = { left, top, right: left, bottom: top };
+ }
+
/**
* Set the pointer events to none on the screenshots elements so
* elementFromPoint can find the real element at the given point.
@@ -1500,8 +1848,10 @@ export class ScreenshotsOverlay {
* scrollHeight: The height of the entire page
* scrollX: The X scroll offset of the viewport
* scrollY: The Y scroll offest of the viewport
- * scrollMinX: The X mininmun the viewport can scroll to
- * scrollMinY: The Y mininmun the viewport can scroll to
+ * scrollMinX: The X minimum the viewport can scroll to
+ * scrollMinY: The Y minimum the viewport can scroll to
+ * scrollMaxX: The X maximum the viewport can scroll to
+ * scrollMaxY: The Y maximum the viewport can scroll to
* }
*/
getDimensionsFromWindow() {
@@ -1542,6 +1892,8 @@ export class ScreenshotsOverlay {
scrollY,
scrollMinX,
scrollMinY,
+ scrollMaxX,
+ scrollMaxY,
};
}
@@ -1570,6 +1922,8 @@ export class ScreenshotsOverlay {
scrollY,
scrollMinX,
scrollMinY,
+ scrollMaxX,
+ scrollMaxY,
} = this.getDimensionsFromWindow();
this.screenshotsContainer.toggleAttribute("resizing", false);
@@ -1582,6 +1936,8 @@ export class ScreenshotsOverlay {
scrollY,
scrollMinX,
scrollMinY,
+ scrollMaxX,
+ scrollMaxY,
devicePixelRatio: this.window.devicePixelRatio,
};
diff --git a/browser/components/screenshots/ScreenshotsUtils.sys.mjs b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
index fc84facee3..4ba925366d 100644
--- a/browser/components/screenshots/ScreenshotsUtils.sys.mjs
+++ b/browser/components/screenshots/ScreenshotsUtils.sys.mjs
@@ -91,6 +91,7 @@ export class ScreenshotsComponentParent extends JSWindowActorParent {
case "Screenshots:OverlaySelection":
ScreenshotsUtils.setPerBrowserState(browser, {
hasOverlaySelection: message.data.hasSelection,
+ overlayState: message.data.overlayState,
});
break;
case "Screenshots:ShowPanel":
@@ -99,6 +100,9 @@ export class ScreenshotsComponentParent extends JSWindowActorParent {
case "Screenshots:HidePanel":
ScreenshotsUtils.closePanel(browser);
break;
+ case "Screenshots:MoveFocusToParent":
+ ScreenshotsUtils.focusPanel(browser, message.data);
+ break;
}
}
@@ -191,11 +195,7 @@ export var ScreenshotsUtils = {
handleEvent(event) {
switch (event.type) {
case "keydown":
- if (event.key === "Escape") {
- // Escape should cancel and exit
- let browser = event.view.gBrowser.selectedBrowser;
- this.cancel(browser, "escape");
- }
+ this.handleKeyDownEvent(event);
break;
case "TabSelect":
this.handleTabSelect(event);
@@ -209,6 +209,33 @@ export var ScreenshotsUtils = {
}
},
+ handleKeyDownEvent(event) {
+ let browser =
+ event.view.browsingContext.topChromeWindow.gBrowser.selectedBrowser;
+ if (!browser) {
+ return;
+ }
+
+ switch (event.key) {
+ case "Escape":
+ // The chromeEventHandler in the child actor will handle events that
+ // don't match this
+ if (event.target.parentElement === this.panelForBrowser(browser)) {
+ this.cancel(browser, "escape");
+ }
+ break;
+ case "ArrowLeft":
+ case "ArrowUp":
+ case "ArrowRight":
+ case "ArrowDown":
+ this.handleArrowKeyDown(event, browser);
+ break;
+ case "Tab":
+ this.maybeLockFocus(event);
+ break;
+ }
+ },
+
/**
* When we swap docshells for a given screenshots browser, we need to update
* the browserToScreenshotsState WeakMap to the correct browser. If the old
@@ -273,6 +300,105 @@ export var ScreenshotsUtils = {
}
},
+ /**
+ * If the overlay state is crosshairs or dragging, move the native cursor
+ * respective to the arrow key pressed.
+ * @param {Event} event A keydown event
+ * @param {Browser} browser The selected browser
+ * @returns
+ */
+ handleArrowKeyDown(event, browser) {
+ // Wayland doesn't support `sendNativeMouseEvent` so just return
+ if (Services.appinfo.isWayland) {
+ return;
+ }
+
+ let { overlayState } = this.browserToScreenshotsState.get(browser);
+
+ if (!["crosshairs", "dragging"].includes(overlayState)) {
+ return;
+ }
+
+ let left = 0;
+ let top = 0;
+ let exponent = event.shiftKey ? 1 : 0;
+ switch (event.key) {
+ case "ArrowLeft":
+ left -= 10 ** exponent;
+ break;
+ case "ArrowUp":
+ top -= 10 ** exponent;
+ break;
+ case "ArrowRight":
+ left += 10 ** exponent;
+ break;
+ case "ArrowDown":
+ top += 10 ** exponent;
+ break;
+ default:
+ return;
+ }
+
+ // Clear and move focus to browser so the child actor can capture events
+ this.clearContentFocus(browser);
+ Services.focus.clearFocus(browser.ownerGlobal);
+ Services.focus.setFocus(browser, 0);
+
+ let x = {};
+ let y = {};
+ let win = browser.ownerGlobal;
+ win.windowUtils.getLastOverWindowPointerLocationInCSSPixels(x, y);
+
+ this.moveCursor(
+ {
+ left: (x.value + left) * win.devicePixelRatio,
+ top: (y.value + top) * win.devicePixelRatio,
+ },
+ browser
+ );
+ },
+
+ /**
+ * Move the native cursor to the given position. Clamp the position to the
+ * window just in case.
+ * @param {Object} position An object containing the left and top position
+ * @param {Browser} browser The selected browser
+ */
+ moveCursor(position, browser) {
+ let { left, top } = position;
+ let win = browser.ownerGlobal;
+
+ const windowLeft = win.mozInnerScreenX * win.devicePixelRatio;
+ const windowTop = win.mozInnerScreenY * win.devicePixelRatio;
+ const contentTop =
+ (win.mozInnerScreenY + (win.innerHeight - browser.clientHeight)) *
+ win.devicePixelRatio;
+ const windowRight =
+ (win.mozInnerScreenX + win.innerWidth) * win.devicePixelRatio;
+ const windowBottom =
+ (win.mozInnerScreenY + win.innerHeight) * win.devicePixelRatio;
+
+ left += windowLeft;
+ top += windowTop;
+
+ // Clamp left and top to content dimensions
+ let parsedLeft = Math.round(
+ Math.min(Math.max(left, windowLeft), windowRight)
+ );
+ let parsedTop = Math.round(
+ Math.min(Math.max(top, contentTop), windowBottom)
+ );
+
+ win.windowUtils.sendNativeMouseEvent(
+ parsedLeft,
+ parsedTop,
+ win.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE,
+ 0,
+ 0,
+ win.document.documentElement
+ );
+ },
+
observe(subj, topic, data) {
let { gBrowser } = subj;
let browser = gBrowser.selectedBrowser;
@@ -335,6 +461,7 @@ export var ScreenshotsUtils = {
browser.addEventListener("SwapDocShells", this);
let gBrowser = browser.getTabBrowser();
gBrowser.tabContainer.addEventListener("TabSelect", this);
+ browser.ownerDocument.addEventListener("keydown", this);
break;
}
case UIPhases.INITIAL:
@@ -364,6 +491,7 @@ export var ScreenshotsUtils = {
browser.removeEventListener("SwapDocShells", this);
const gBrowser = browser.getTabBrowser();
gBrowser.tabContainer.removeEventListener("TabSelect", this);
+ browser.ownerDocument.removeEventListener("keydown", this);
this.browserToScreenshotsState.delete(browser);
if (Cu.isInAutomation) {
@@ -396,6 +524,53 @@ export var ScreenshotsUtils = {
Object.assign(perBrowserState, nameValues);
},
+ maybeLockFocus(event) {
+ let browser = event.view.gBrowser.selectedBrowser;
+
+ if (!Services.focus.focusedElement) {
+ event.preventDefault();
+ this.focusPanel(browser);
+ return;
+ }
+
+ let target = event.explicitOriginalTarget;
+
+ if (!target.closest("moz-button-group")) {
+ return;
+ }
+
+ let isElementFirst = !!target.nextElementSibling;
+
+ if (
+ (isElementFirst && event.shiftKey) ||
+ (!isElementFirst && !event.shiftKey)
+ ) {
+ event.preventDefault();
+ this.moveFocusToContent(browser);
+ }
+ },
+
+ focusPanel(browser, { direction } = {}) {
+ let buttonsPanel = this.panelForBrowser(browser);
+ if (direction) {
+ buttonsPanel
+ .querySelector("screenshots-buttons")
+ .focusButton(direction === "forward" ? "first" : "last");
+ } else {
+ buttonsPanel
+ .querySelector("screenshots-buttons")
+ .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
+ }
+ },
+
+ moveFocusToContent(browser) {
+ this.getActor(browser).sendAsyncMessage("Screenshots:MoveFocusToContent");
+ },
+
+ clearContentFocus(browser) {
+ this.getActor(browser).sendAsyncMessage("Screenshots:ClearFocus");
+ },
+
/**
* Attempt to place focus on the element that had focus before screenshots UI was shown
*
@@ -510,7 +685,7 @@ export var ScreenshotsUtils = {
async openPreviewDialog(browser) {
let dialogBox = browser.ownerGlobal.gBrowser.getTabDialogBox(browser);
let { dialog, closedPromise } = await dialogBox.open(
- `chrome://browser/content/screenshots/screenshots.html?browsingContextId=${browser.browsingContext.id}`,
+ `chrome://browser/content/screenshots/screenshots-preview.html?browsingContextId=${browser.browsingContext.id}`,
{
features: "resizable=no",
sizeTo: "available",
@@ -586,14 +761,18 @@ export var ScreenshotsUtils = {
openPanel(browser) {
let buttonsPanel = this.panelForBrowser(browser);
if (!buttonsPanel.hidden) {
- return;
+ return null;
}
buttonsPanel.hidden = false;
- buttonsPanel.ownerDocument.addEventListener("keydown", this);
- buttonsPanel
- .querySelector("screenshots-buttons")
- .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
+ return new Promise(resolve => {
+ browser.ownerGlobal.requestAnimationFrame(() => {
+ buttonsPanel
+ .querySelector("screenshots-buttons")
+ .focusButton(lazy.SCREENSHOTS_LAST_SCREENSHOT_METHOD);
+ resolve();
+ });
+ });
},
/**
@@ -606,7 +785,6 @@ export var ScreenshotsUtils = {
return;
}
buttonsPanel.hidden = true;
- buttonsPanel.ownerDocument.removeEventListener("keydown", this);
},
/**
@@ -652,7 +830,6 @@ export var ScreenshotsUtils = {
let currTabDialogBox = browser.tabDialogBox;
let browserContextId = browser.browsingContext.id;
if (currTabDialogBox) {
- currTabDialogBox.getTabDialogManager();
let manager = currTabDialogBox.getTabDialogManager();
let dialogs = manager.hasDialogs && manager.dialogs;
if (dialogs.length) {
@@ -661,7 +838,7 @@ export var ScreenshotsUtils = {
dialog._openedURL.endsWith(
`browsingContextId=${browserContextId}`
) &&
- dialog._openedURL.includes("screenshots.html")
+ dialog._openedURL.includes("screenshots-preview.html")
) {
return dialog;
}
@@ -817,11 +994,11 @@ export var ScreenshotsUtils = {
let dialog = await this.openPreviewDialog(browser);
await dialog._dialogReady;
- let screenshotsUI =
- dialog._frame.contentDocument.createElement("screenshots-ui");
- dialog._frame.contentDocument.body.appendChild(screenshotsUI);
+ let screenshotsPreviewEl = dialog._frame.contentDocument.querySelector(
+ "screenshots-preview"
+ );
- screenshotsUI.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD);
+ screenshotsPreviewEl.focusButton(lazy.SCREENSHOTS_LAST_SAVED_METHOD);
let rect;
let lastUsedMethod;
@@ -851,15 +1028,12 @@ export var ScreenshotsUtils = {
async takeScreenshot(browser, dialog, rect) {
let canvas = await this.createCanvas(rect, browser);
- let newImg = dialog._frame.contentDocument.createElement("img");
let url = canvas.toDataURL();
+ let screenshotsPreviewEl = dialog._frame.contentDocument.querySelector(
+ "screenshots-preview"
+ );
- newImg.id = "placeholder-image";
-
- newImg.src = url;
- dialog._frame.contentDocument
- .getElementById("preview-image-div")
- .appendChild(newImg);
+ screenshotsPreviewEl.previewImg.src = url;
if (Cu.isInAutomation) {
Services.obs.notifyObservers(null, "screenshots-preview-ready");
@@ -1050,14 +1224,19 @@ export var ScreenshotsUtils = {
* @param dataUrl The image data
* @param browser The current browser
* @param data Telemetry data
+ * @returns true if the download succeeds, otherwise false
*/
async downloadScreenshot(title, dataUrl, browser, data) {
// Guard against missing image data.
if (!dataUrl) {
- return;
+ return false;
}
- let filename = await getFilename(title, browser);
+ let { filename, accepted } = await getFilename(title, browser);
+
+ if (!accepted) {
+ return false;
+ }
const targetFile = new lazy.FileUtils.File(filename);
@@ -1079,7 +1258,15 @@ export var ScreenshotsUtils = {
// Await successful completion of the save via the download manager
await download.start();
- } catch (ex) {}
+ } catch (ex) {
+ console.error(
+ `Failed to create download using filename: ${filename} (length: ${
+ new Blob([filename]).size
+ })`
+ );
+
+ return false;
+ }
let extra = await this.getActor(browser).sendQuery(
"Screenshots:GetMethodsUsed"
@@ -1094,6 +1281,8 @@ export var ScreenshotsUtils = {
SCREENSHOTS_LAST_SAVED_METHOD_PREF,
"download"
);
+
+ return true;
},
recordTelemetryEvent(type, object, args) {
diff --git a/browser/components/screenshots/content/screenshots.css b/browser/components/screenshots/content/screenshots.css
deleted file mode 100644
index 506f3658c9..0000000000
--- a/browser/components/screenshots/content/screenshots.css
+++ /dev/null
@@ -1,68 +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/. */
-
-html,
-body {
- height: 100vh;
- width: 100vw;
-}
-
-.image-view {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-.preview-buttons {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- width: 100%;
- border: 0;
- box-sizing: border-box;
- margin: 4px 0;
- margin-inline-start: calc(-2% + 4px);
-}
-
-.preview-button {
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- text-align: center;
- user-select: none;
- white-space: nowrap;
- min-height: 36px;
- font-size: 15px;
- min-width: 36px;
-}
-
-.preview-button > img {
- -moz-context-properties: fill;
- fill: currentColor;
- width: 16px;
- height: 16px;
-}
-
-#download > img,
-#copy > img {
- margin-inline-end: 5px;
-}
-
-.preview-image {
- height: 100%;
- width: 100%;
- overflow: auto;
-}
-
-#preview-image-div {
- margin: 2%;
- margin-top: 0;
-}
-
-#placeholder-image {
- width: 100%;
- height: 100%;
-}
diff --git a/browser/components/screenshots/content/screenshots.html b/browser/components/screenshots/content/screenshots.html
deleted file mode 100644
index 88c71fb4fe..0000000000
--- a/browser/components/screenshots/content/screenshots.html
+++ /dev/null
@@ -1,68 +0,0 @@
-<!DOCTYPE html>
-<!-- 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/. -->
-<html>
- <head>
- <meta charset="utf-8" />
- <title></title>
- <meta
- http-equiv="Content-Security-Policy"
- content="default-src chrome:;img-src data:; object-src 'none'"
- />
-
- <link rel="localization" href="browser/screenshots.ftl" />
-
- <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
- <link
- rel="stylesheet"
- href="chrome://browser/content/screenshots/screenshots.css"
- />
- <script
- defer
- src="chrome://browser/content/screenshots/screenshots.js"
- ></script>
- </head>
-
- <body>
- <template id="screenshots-dialog-template">
- <div class="image-view">
- <div class="preview-buttons">
- <button
- id="retry"
- class="preview-button"
- data-l10n-id="screenshots-retry-button-title"
- >
- <img src="chrome://global/skin/icons/reload.svg" />
- </button>
- <button
- id="cancel"
- class="preview-button"
- data-l10n-id="screenshots-cancel-button-title"
- >
- <img src="chrome://global/skin/icons/close.svg" />
- </button>
- <button
- id="copy"
- class="preview-button"
- data-l10n-id="screenshots-copy-button-title"
- >
- <img src="chrome://global/skin/icons/edit-copy.svg" />
- <span data-l10n-id="screenshots-copy-button" />
- </button>
- <button
- id="download"
- class="preview-button primary"
- data-l10n-id="screenshots-download-button-title"
- >
- <img src="chrome://browser/skin/downloads/downloads.svg" />
- <span data-l10n-id="screenshots-download-button" />
- </button>
- </div>
- <div class="preview-image">
- <div id="preview-image-div"></div>
- </div>
- </div>
- </template>
- </body>
-</html>
diff --git a/browser/components/screenshots/content/screenshots.js b/browser/components/screenshots/content/screenshots.js
deleted file mode 100644
index 9e47570e07..0000000000
--- a/browser/components/screenshots/content/screenshots.js
+++ /dev/null
@@ -1,105 +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/browser-window */
-
-"use strict";
-
-ChromeUtils.defineESModuleGetters(this, {
- ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
-});
-
-class ScreenshotsUI extends HTMLElement {
- constructor() {
- super();
- // we get passed the <browser> as a param via TabDialogBox.open()
- this.openerBrowser = window.arguments[0];
- }
- async connectedCallback() {
- this.initialize();
- }
-
- initialize() {
- if (this._initialized) {
- return;
- }
- this._initialized = true;
- let template = this.ownerDocument.getElementById(
- "screenshots-dialog-template"
- );
- let templateContent = template.content;
- this.appendChild(templateContent.cloneNode(true));
-
- this._retryButton = this.querySelector("#retry");
- this._retryButton.addEventListener("click", this);
- this._cancelButton = this.querySelector("#cancel");
- this._cancelButton.addEventListener("click", this);
- this._copyButton = this.querySelector("#copy");
- this._copyButton.addEventListener("click", this);
- this._downloadButton = this.querySelector("#download");
- this._downloadButton.addEventListener("click", this);
- }
-
- close() {
- URL.revokeObjectURL(document.getElementById("placeholder-image").src);
- 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();
- }
- }
-
- async saveToFile(dataUrl) {
- await ScreenshotsUtils.downloadScreenshot(
- null,
- dataUrl,
- this.openerBrowser,
- { object: "preview_download" }
- );
- this.close();
- }
-
- async saveToClipboard(dataUrl) {
- await ScreenshotsUtils.copyScreenshot(dataUrl, this.openerBrowser, {
- object: "preview_copy",
- });
- this.close();
- }
-
- /**
- * Set the focus to the most recent saved method.
- * This will default to the download button.
- * @param {String} buttonToFocus
- */
- focusButton(buttonToFocus) {
- if (buttonToFocus === "copy") {
- this._copyButton.focus({ focusVisible: true });
- } else {
- this._downloadButton.focus({ focusVisible: true });
- }
- }
-}
-customElements.define("screenshots-ui", ScreenshotsUI);
diff --git a/browser/components/screenshots/fileHelpers.mjs b/browser/components/screenshots/fileHelpers.mjs
index 4fd2e77561..f416ca195a 100644
--- a/browser/components/screenshots/fileHelpers.mjs
+++ b/browser/components/screenshots/fileHelpers.mjs
@@ -7,11 +7,15 @@ const { AppConstants } = ChromeUtils.importESModule(
);
const lazy = {};
+// The maximum length of a pathanme - calculated as MAX_PATH minus the null terminator character
+export const MAX_PATHNAME = AppConstants.platform == "win" ? 259 : 1023;
+export const MAX_LEAFNAME = MAX_PATHNAME - 32;
+export const FALLBACK_MAX_LEAFNAME = 64;
ChromeUtils.defineESModuleGetters(lazy, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
DownloadLastDir: "resource://gre/modules/DownloadLastDir.sys.mjs",
DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
- Downloads: "resource://gre/modules/Downloads.sys.mjs",
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
});
@@ -29,6 +33,15 @@ export async function getFilename(filenameTitle, browser) {
);
}
const date = new Date();
+ const knownDownloadsDir = await getDownloadDirectory();
+ // if we know the download directory, we can subtract that plus the separator from MAX_PATHNAME to get a length limit
+ // otherwise we just use a conservative length
+ const maxFilenameLength = Math.min(
+ knownDownloadsDir
+ ? MAX_PATHNAME - new Blob([knownDownloadsDir]).size - 1
+ : FALLBACK_MAX_LEAFNAME,
+ MAX_LEAFNAME
+ );
/* eslint-disable no-control-regex */
filenameTitle = filenameTitle
.replace(/[\\/]/g, "_")
@@ -44,43 +57,37 @@ export async function getFilename(filenameTitle, browser) {
const filenameTime = currentDateTime.substring(11, 19).replace(/:/g, "-");
let clipFilename = `Screenshot ${filenameDate} at ${filenameTime} ${filenameTitle}`;
- // Crop the filename size at less than 246 bytes, so as to leave
+ // allow space for a potential ellipsis and the extension
+ let maxNameStemLength = maxFilenameLength - "[...].png".length;
+
+ // Crop the filename size so as to leave
// room for the extension and an ellipsis [...]. Note that JS
// strings are UTF16 but the filename will be converted to UTF8
// when saving which could take up more space, and we want a
- // maximum of 255 bytes (not characters). Here, we iterate
+ // maximum of maxFilenameLength bytes (not characters). Here, we iterate
// and crop at shorter and shorter points until we fit into
- // 255 bytes.
+ // our max number of bytes.
let suffix = "";
- for (let cropSize = 246; cropSize >= 0; cropSize -= 32) {
- if (new Blob([clipFilename]).size > 246) {
+ for (let cropSize = maxNameStemLength; cropSize >= 0; cropSize -= 32) {
+ if (new Blob([clipFilename]).size > maxNameStemLength) {
clipFilename = clipFilename.substring(0, cropSize);
suffix = "[...]";
} else {
break;
}
}
-
clipFilename += suffix;
let extension = ".png";
let filename = clipFilename + extension;
- let useDownloadDir = Services.prefs.getBoolPref(
- "browser.download.useDownloadDir"
- );
- if (useDownloadDir) {
- const downloadsDir = await lazy.Downloads.getPreferredDownloadsDirectory();
- const downloadsDirExists = await IOUtils.exists(downloadsDir);
- if (downloadsDirExists) {
- // If filename is absolute, it will override the downloads directory and
- // still be applied as expected.
- filename = PathUtils.join(downloadsDir, filename);
- }
+ if (knownDownloadsDir) {
+ // If filename is absolute, it will override the downloads directory and
+ // still be applied as expected.
+ filename = PathUtils.join(knownDownloadsDir, filename);
} else {
let fileInfo = new FileInfo(filename);
let file;
-
let fpParams = {
fpTitleKey: "SaveImageTitle",
fileInfo,
@@ -88,16 +95,30 @@ export async function getFilename(filenameTitle, browser) {
saveAsType: 0,
file,
};
-
let accepted = await promiseTargetFile(fpParams, browser.ownerGlobal);
if (!accepted) {
- return null;
+ return { filename: null, accepted };
}
-
filename = fpParams.file.path;
}
+ return { filename, accepted: true };
+}
- return filename;
+/**
+ * Gets the path to the download directory if "browser.download.useDownloadDir" is true
+ * @returns Path to download directory or null if not available
+ */
+export async function getDownloadDirectory() {
+ let useDownloadDir = Services.prefs.getBoolPref(
+ "browser.download.useDownloadDir"
+ );
+ if (useDownloadDir) {
+ const downloadsDir = await lazy.Downloads.getPreferredDownloadsDirectory();
+ if (await IOUtils.exists(downloadsDir)) {
+ return downloadsDir;
+ }
+ }
+ return null;
}
// The below functions are a modified copy from toolkit/content/contentAreaUtils.js
diff --git a/browser/components/screenshots/jar.mn b/browser/components/screenshots/jar.mn
index 7a4e2ed73a..4618f78c52 100644
--- a/browser/components/screenshots/jar.mn
+++ b/browser/components/screenshots/jar.mn
@@ -12,11 +12,11 @@ browser.jar:
content/browser/screenshots/icon-welcome-face-without-eyes.svg (content/icon-welcome-face-without-eyes.svg)
content/browser/screenshots/menu-fullpage.svg (content/menu-fullpage.svg)
content/browser/screenshots/menu-visible.svg (content/menu-visible.svg)
- content/browser/screenshots/screenshots.js (content/screenshots.js)
content/browser/screenshots/screenshots-buttons.js (screenshots-buttons.js)
content/browser/screenshots/screenshots-buttons.css (screenshots-buttons.css)
- content/browser/screenshots/screenshots.css (content/screenshots.css)
- content/browser/screenshots/screenshots.html (content/screenshots.html)
+ content/browser/screenshots/screenshots-preview.css (screenshots-preview.css)
+ content/browser/screenshots/screenshots-preview.html (screenshots-preview.html)
+ content/browser/screenshots/screenshots-preview.mjs (screenshots-preview.mjs)
content/browser/screenshots/overlay/ (overlay/**)
content/browser/screenshots/overlayHelpers.mjs
diff --git a/browser/components/screenshots/overlay/overlay.css b/browser/components/screenshots/overlay/overlay.css
index 6eeda8b44c..d8aeb1f907 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] {
@@ -47,7 +53,7 @@
justify-content: center;
position: sticky;
top: 0;
- left: 0;
+ inset-inline: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.7);
@@ -57,6 +63,7 @@
position: absolute;
margin: 10px 0;
cursor: auto;
+ z-index: var(--screenshots-highest-layer);
}
#selection-size,
@@ -67,6 +74,11 @@
border-radius: 4px;
}
+#selection-size {
+ border: var(--border-width) solid var(--in-content-border-color);
+ box-shadow: var(--shadow-30);
+}
+
.buttons-wrapper,
#selection-size-container {
display: flex;
@@ -77,11 +89,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 +104,7 @@
width: 100%;
height: 100%;
pointer-events: none;
+ z-index: var(--screenshots-lowest-layer);
}
#screenshots-cancel-button {
@@ -97,7 +112,7 @@
border-color: #fff;
color: #fff;
- @media (prefers-contrast) {
+ @media (forced-colors) {
background-color: var(--in-content-button-background);
color: var(--in-content-button-text-color);
border-color: var(--in-content-button-border-color);
@@ -108,7 +123,7 @@
background-color: #fff;
color: #000;
- @media (prefers-contrast) {
+ @media (forced-colors) {
background-color: var(--in-content-button-background-hover);
color: var(--in-content-button-text-color-hover);
border-color: var(--in-content-button-border-color-hover);
@@ -123,6 +138,10 @@
pointer-events: none;
}
+.screenshots-button > label {
+ pointer-events: none;
+}
+
#cancel > img {
content: url("chrome://global/skin/icons/close.svg");
}
@@ -135,15 +154,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 (forced-colors) {
+ display: none;
+ }
}
.face {
@@ -172,7 +190,7 @@
border-radius: 50%;
inset-inline-start: 2px;
top: 4px;
- z-index: 10;
+ z-index: var(--screenshots-high-layer);
}
.left {
@@ -195,7 +213,7 @@
padding: 20px;
width: 400px;
- @media (prefers-contrast) {
+ @media (forced-colors) {
color: CanvasText;
background-color: Canvas;
}
@@ -209,7 +227,7 @@
box-sizing: border-box;
pointer-events: none;
position: absolute;
- z-index: 11;
+ z-index: var(--screenshots-high-layer);
}
#top-background {
@@ -242,7 +260,7 @@
cursor: move;
position: absolute;
pointer-events: auto;
- z-index: 2;
+ z-index: var(--screenshots-lowest-layer);
outline-offset: 8px;
}
@@ -251,7 +269,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 +288,7 @@
inset-inline-start: 0;
top: -30px;
width: 100%;
- z-index: 4;
+ z-index: var(--screenshots-low-layer);
}
.mover-target.direction-topRight {
@@ -287,7 +305,7 @@
left: -30px;
top: 0;
width: 60px;
- z-index: 4;
+ z-index: var(--screenshots-low-layer);
}
.mover-target.direction-right {
@@ -296,7 +314,7 @@
right: -30px;
top: 0;
width: 60px;
- z-index: 4;
+ z-index: var(--screenshots-low-layer);
}
.mover-target.direction-bottomLeft {
@@ -313,7 +331,7 @@
height: 60px;
inset-inline-start: 0;
width: 100%;
- z-index: 4;
+ z-index: var(--screenshots-low-layer);
}
.mover-target.direction-bottomRight {
@@ -338,7 +356,7 @@
width: 16px;
pointer-events: none;
- @media (prefers-contrast) {
+ @media (forced-colors) {
background-color: ButtonText;
}
}
diff --git a/browser/components/screenshots/overlayHelpers.mjs b/browser/components/screenshots/overlayHelpers.mjs
index 70a1bd86d0..e91200a8f5 100644
--- a/browser/components/screenshots/overlayHelpers.mjs
+++ b/browser/components/screenshots/overlayHelpers.mjs
@@ -402,6 +402,8 @@ export class WindowDimensions {
#scrollY = null;
#scrollMinX = null;
#scrollMinY = null;
+ #scrollMaxX = null;
+ #scrollMaxY = null;
#devicePixelRatio = null;
set dimensions(dimensions) {
@@ -429,6 +431,12 @@ export class WindowDimensions {
if (dimensions.scrollMinY != null) {
this.#scrollMinY = dimensions.scrollMinY;
}
+ if (dimensions.scrollMaxX != null) {
+ this.#scrollMaxX = dimensions.scrollMaxX;
+ }
+ if (dimensions.scrollMaxY != null) {
+ this.#scrollMaxY = dimensions.scrollMaxY;
+ }
if (dimensions.devicePixelRatio != null) {
this.#devicePixelRatio = dimensions.devicePixelRatio;
}
@@ -436,15 +444,19 @@ export class WindowDimensions {
get dimensions() {
return {
- clientHeight: this.#clientHeight,
- clientWidth: this.#clientWidth,
- scrollHeight: this.#scrollHeight,
- scrollWidth: this.#scrollWidth,
- scrollX: this.#scrollX,
- scrollY: this.#scrollY,
- scrollMinX: this.#scrollMinX,
- scrollMinY: this.#scrollMinY,
- devicePixelRatio: this.#devicePixelRatio,
+ clientHeight: this.clientHeight,
+ clientWidth: this.clientWidth,
+ scrollHeight: this.scrollHeight,
+ scrollWidth: this.scrollWidth,
+ scrollX: this.scrollX,
+ scrollY: this.scrollY,
+ pageScrollX: this.pageScrollX,
+ pageScrollY: this.pageScrollY,
+ scrollMinX: this.scrollMinX,
+ scrollMinY: this.scrollMinY,
+ scrollMaxX: this.scrollMaxX,
+ scrollMaxY: this.scrollMaxY,
+ devicePixelRatio: this.devicePixelRatio,
};
}
@@ -465,10 +477,18 @@ export class WindowDimensions {
}
get scrollX() {
+ return this.#scrollX - this.scrollMinX;
+ }
+
+ get pageScrollX() {
return this.#scrollX;
}
get scrollY() {
+ return this.#scrollY - this.scrollMinY;
+ }
+
+ get pageScrollY() {
return this.#scrollY;
}
@@ -480,10 +500,33 @@ export class WindowDimensions {
return this.#scrollMinY;
}
+ get scrollMaxX() {
+ return this.#scrollMaxX;
+ }
+
+ get scrollMaxY() {
+ return this.#scrollMaxY;
+ }
+
get devicePixelRatio() {
return this.#devicePixelRatio;
}
+ isInViewport(rect) {
+ // eslint-disable-next-line no-shadow
+ let { left, top, right, bottom } = rect;
+
+ if (
+ left > this.scrollX + this.clientWidth ||
+ right < this.scrollX ||
+ top > this.scrollY + this.clientHeight ||
+ bottom < this.scrollY
+ ) {
+ return false;
+ }
+ return true;
+ }
+
reset() {
this.#clientHeight = 0;
this.#clientWidth = 0;
@@ -493,5 +536,7 @@ export class WindowDimensions {
this.#scrollY = 0;
this.#scrollMinX = 0;
this.#scrollMinY = 0;
+ this.#scrollMaxX = 0;
+ this.#scrollMaxY = 0;
}
}
diff --git a/browser/components/screenshots/screenshots-buttons.css b/browser/components/screenshots/screenshots-buttons.css
index 82b075bccb..ccb092174e 100644
--- a/browser/components/screenshots/screenshots-buttons.css
+++ b/browser/components/screenshots/screenshots-buttons.css
@@ -14,18 +14,19 @@
border-radius: var(--arrowpanel-border-radius);
}
-.full-page {
+#full-page {
background-image: url("chrome://browser/content/screenshots/menu-fullpage.svg");
}
-.visible-page {
+#visible-page {
background-image: url("chrome://browser/content/screenshots/menu-visible.svg");
}
-.full-page, .visible-page {
+#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..e501da5a51 100644
--- a/browser/components/screenshots/screenshots-buttons.js
+++ b/browser/components/screenshots/screenshots-buttons.js
@@ -13,28 +13,41 @@
});
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 id="visible-page" class="screenshot-button footer-button" data-l10n-id="screenshots-save-visible-button"></html:button>
+ <html:button id="full-page" class="screenshot-button 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 = shadowRoot.getElementById("visible-page");
visibleButton.onclick = function () {
ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "visible");
};
- let fullpageButton = shadowRoot.querySelector(".full-page");
+ let fullpageButton = shadowRoot.getElementById("full-page");
fullpageButton.onclick = function () {
ScreenshotsUtils.doScreenshot(gBrowser.selectedBrowser, "full_page");
};
@@ -49,14 +62,23 @@
* 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")
+ .getElementById("full-page")
.focus({ focusVisible: true });
+ } else if (buttonToFocus === "first") {
+ this.shadowRoot
+ .querySelector("moz-button-group")
+ .firstElementChild.focus({ focusVisible: true });
+ } else if (buttonToFocus === "last") {
+ this.shadowRoot
+ .querySelector("moz-button-group")
+ .lastElementChild.focus({ focusVisible: true });
} else {
this.shadowRoot
- .querySelector(".visible-page")
+ .getElementById("visible-page")
.focus({ focusVisible: true });
}
}
diff --git a/browser/components/screenshots/screenshots-preview.css b/browser/components/screenshots/screenshots-preview.css
new file mode 100644
index 0000000000..f69392f42b
--- /dev/null
+++ b/browser/components/screenshots/screenshots-preview.css
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+:host {
+ height: 100vh;
+ width: 100vw;
+ display: block;
+}
+
+.image-view {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.preview-buttons {
+ display: flex;
+ gap: var(--space-small);
+ align-items: center;
+ justify-content: flex-end;
+ width: 98%;
+ padding: var(--space-small) 0;
+}
+
+#preview-image-container {
+ height: 100vh;
+ overflow: auto;
+ padding: 2%;
+ padding-top: 0;
+}
+
+#preview-image {
+ width: 100%;
+}
diff --git a/browser/components/screenshots/screenshots-preview.html b/browser/components/screenshots/screenshots-preview.html
new file mode 100644
index 0000000000..83ddbae08f
--- /dev/null
+++ b/browser/components/screenshots/screenshots-preview.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<!-- 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/. -->
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:;img-src data:; object-src 'none'"
+ />
+
+ <link rel="localization" href="browser/screenshots.ftl" />
+ <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
+ <script
+ type="module"
+ src="chrome://browser/content/screenshots/screenshots-preview.mjs"
+ ></script>
+ </head>
+
+ <body>
+ <screenshots-preview></screenshots-preview>
+ </body>
+</html>
diff --git a/browser/components/screenshots/screenshots-preview.mjs b/browser/components/screenshots/screenshots-preview.mjs
new file mode 100644
index 0000000000..0609bac959
--- /dev/null
+++ b/browser/components/screenshots/screenshots-preview.mjs
@@ -0,0 +1,271 @@
+/* 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";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "screenshotsLocalization", () => {
+ return new Localization(["browser/screenshots.ftl"], true);
+});
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
+ ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
+ ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
+});
+
+class ScreenshotsPreview extends MozLitElement {
+ static queries = {
+ retryButtonEl: "#retry",
+ cancelButtonEl: "#cancel",
+ copyButtonEl: "#copy",
+ downloadButtonEl: "#download",
+ previewImg: "#preview-image",
+ buttons: { all: "moz-button" },
+ };
+
+ constructor() {
+ super();
+ // we get passed the <browser> as a param via TabDialogBox.open()
+ this.openerBrowser = window.arguments[0];
+
+ let [downloadKey, copyKey] =
+ lazy.screenshotsLocalization.formatMessagesSync([
+ { id: "screenshots-component-download-key" },
+ { id: "screenshots-component-copy-key" },
+ ]);
+
+ this.downloadKey = downloadKey.value;
+ this.copyKey = copyKey.value;
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ window.addEventListener("keydown", this, true);
+
+ this.updateL10nAttributes();
+ }
+
+ async updateL10nAttributes() {
+ let accelString = lazy.ShortcutUtils.getModifierString("accel");
+ let copyShorcut = accelString + this.copyKey;
+ let downloadShortcut = accelString + this.downloadKey;
+
+ await this.updateComplete;
+
+ document.l10n.setAttributes(
+ this.copyButtonEl,
+ "screenshots-component-copy-button-2",
+ { shortcut: copyShorcut }
+ );
+
+ document.l10n.setAttributes(
+ this.downloadButtonEl,
+ "screenshots-component-download-button-2",
+ { shortcut: downloadShortcut }
+ );
+ }
+
+ close() {
+ window.removeEventListener("keydown", this, true);
+ URL.revokeObjectURL(this.previewImg.src);
+ window.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":
+ lazy.ScreenshotsUtils.scheduleRetry(
+ this.openerBrowser,
+ "preview_retry"
+ );
+ this.close();
+ break;
+ case "cancel":
+ this.close();
+ lazy.ScreenshotsUtils.recordTelemetryEvent(
+ "canceled",
+ "preview_cancel",
+ {}
+ );
+ break;
+ case "copy":
+ this.saveToClipboard();
+ break;
+ case "download":
+ this.saveToFile();
+ break;
+ }
+ }
+
+ handleKeydown(event) {
+ switch (event.key) {
+ case this.copyKey.toLowerCase():
+ if (this.getAccelKey(event)) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.saveToClipboard();
+ }
+ break;
+ case this.downloadKey.toLowerCase():
+ if (this.getAccelKey(event)) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.saveToFile();
+ }
+ break;
+ }
+ }
+
+ /**
+ * If the image is complete and the height is greater than 0, we can resolve.
+ * Otherwise wait for a load event on the image and resolve then.
+ * @returns {Promise<String>} Resolves that resolves to the preview image src
+ * once the image is loaded.
+ */
+ async imageLoadedPromise() {
+ await this.updateComplete;
+ if (this.previewImg.complete && this.previewImg.height > 0) {
+ return Promise.resolve(this.previewImg.src);
+ }
+
+ return new Promise(resolve => {
+ function onImageLoaded(event) {
+ resolve(event.target.src);
+ }
+ this.previewImg.addEventListener("load", onImageLoaded, { once: true });
+ });
+ }
+
+ getAccelKey(event) {
+ if (lazy.AppConstants.platform === "macosx") {
+ return event.metaKey;
+ }
+ return event.ctrlKey;
+ }
+
+ /**
+ * Enable all the buttons. This will only happen when the download button is
+ * clicked and the file picker is closed without saving the image.
+ */
+ enableButtons() {
+ this.buttons.forEach(button => (button.disabled = false));
+ }
+
+ /**
+ * Disable all the buttons so they can't be clicked multiple times before
+ * successfully copying or downloading the image.
+ */
+ disableButtons() {
+ this.buttons.forEach(button => (button.disabled = true));
+ }
+
+ async saveToFile() {
+ // Disable buttons so they can't by clicked again while waiting for the
+ // image to load.
+ this.disableButtons();
+
+ // Wait for the image to be loaded before we save it
+ let imageSrc = await this.imageLoadedPromise();
+ let downloadSucceeded = await lazy.ScreenshotsUtils.downloadScreenshot(
+ null,
+ imageSrc,
+ this.openerBrowser,
+ { object: "preview_download" }
+ );
+
+ if (downloadSucceeded) {
+ this.close();
+ } else {
+ this.enableButtons();
+ }
+ }
+
+ async saveToClipboard() {
+ // Disable buttons so they can't by clicked again while waiting for the
+ // image to load
+ this.disableButtons();
+
+ // Wait for the image to be loaded before we copy it
+ let imageSrc = await this.imageLoadedPromise();
+ await lazy.ScreenshotsUtils.copyScreenshot(imageSrc, this.openerBrowser, {
+ object: "preview_copy",
+ });
+ this.close();
+ }
+
+ /**
+ * Set the focus to the most recent saved method.
+ * This will default to the download button.
+ * @param {String} buttonToFocus
+ */
+ focusButton(buttonToFocus) {
+ if (buttonToFocus === "copy") {
+ this.copyButtonEl.focus({ focusVisible: true });
+ } else {
+ this.downloadButtonEl.focus({ focusVisible: true });
+ }
+ }
+
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/screenshots/screenshots-preview.css"
+ />
+ <div class="image-view">
+ <div class="preview-buttons">
+ <moz-button
+ id="retry"
+ data-l10n-id="screenshots-component-retry-button"
+ iconSrc="chrome://global/skin/icons/reload.svg"
+ @click=${this.handleClick}
+ ></moz-button>
+ <moz-button
+ id="cancel"
+ data-l10n-id="screenshots-component-cancel-button"
+ iconSrc="chrome://global/skin/icons/close.svg"
+ @click=${this.handleClick}
+ ></moz-button>
+ <moz-button
+ id="copy"
+ data-l10n-id="screenshots-component-copy-button-2"
+ data-l10n-args='{ "shortcut": "" }'
+ iconSrc="chrome://global/skin/icons/edit-copy.svg"
+ @click=${this.handleClick}
+ ></moz-button>
+ <moz-button
+ id="download"
+ type="primary"
+ data-l10n-id="screenshots-component-download-button-2"
+ data-l10n-args='{ "shortcut": "" }'
+ iconSrc="chrome://browser/skin/downloads/downloads.svg"
+ @click=${this.handleClick}
+ ></moz-button>
+ </div>
+ <div id="preview-image-container">
+ <img id="preview-image" />
+ </div>
+ </div>
+ `;
+ }
+}
+
+customElements.define("screenshots-preview", ScreenshotsPreview);
diff --git a/browser/components/screenshots/tests/browser/browser.toml b/browser/components/screenshots/tests/browser/browser.toml
index b27d28c677..472bc853d1 100644
--- a/browser/components/screenshots/tests/browser/browser.toml
+++ b/browser/components/screenshots/tests/browser/browser.toml
@@ -8,6 +8,8 @@ support-files = [
"short-test-page.html",
"large-test-page.html",
"test-page-resize.html",
+ "test-selectionAPI-page.html",
+ "rtl-test-page.html",
]
prefs = [
@@ -18,6 +20,14 @@ prefs = [
["browser_iframe_test.js"]
skip-if = ["os == 'linux'"]
+["browser_keyboard_shortcuts.js"]
+skip-if = [
+ "headless",
+ "display == 'wayland'" # sendNativeMouseEvent doesn't work on wayland
+]
+
+["browser_keyboard_tests.js"]
+
["browser_overlay_keyboard_test.js"]
["browser_screenshots_drag_scroll_test.js"]
@@ -26,6 +36,8 @@ skip-if = [
"apple_catalina", # Bug 1804441
]
+["browser_screenshots_download_filenames.js"]
+
["browser_screenshots_drag_test.js"]
["browser_screenshots_focus_test.js"]
@@ -63,3 +75,7 @@ skip-if = ["!crashreporter"]
["browser_test_moving_tab_to_new_window.js"]
["browser_test_resize.js"]
+
+["browser_test_selection_size_text.js"]
+
+["browser_text_selectionAPI_test.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..66ab25f1c6
--- /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_keyboard_tests.js b/browser/components/screenshots/tests/browser/browser_keyboard_tests.js
new file mode 100644
index 0000000000..b2bb6fd16e
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_keyboard_tests.js
@@ -0,0 +1,482 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const KEY_TO_EXPECTED_POSITION_ARRAY = [
+ [
+ "ArrowRight",
+ {
+ top: 100,
+ left: 100,
+ bottom: 100,
+ right: 110,
+ },
+ ],
+ [
+ "ArrowDown",
+ {
+ top: 100,
+ left: 100,
+ bottom: 110,
+ right: 110,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: 100,
+ left: 100,
+ bottom: 110,
+ right: 100,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: 100,
+ left: 100,
+ bottom: 100,
+ right: 100,
+ },
+ ],
+ ["ArrowDown", { top: 100, left: 100, bottom: 110, right: 100 }],
+ [
+ "ArrowRight",
+ {
+ top: 100,
+ left: 100,
+ bottom: 110,
+ right: 110,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: 100,
+ left: 100,
+ bottom: 100,
+ right: 110,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: 100,
+ left: 100,
+ bottom: 100,
+ right: 100,
+ },
+ ],
+];
+
+const SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [
+ [
+ "ArrowRight",
+ {
+ top: 100,
+ left: 100,
+ bottom: 100,
+ right: 200,
+ },
+ ],
+ [
+ "ArrowDown",
+ {
+ top: 100,
+ left: 100,
+ bottom: 200,
+ right: 200,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: 100,
+ left: 100,
+ bottom: 200,
+ right: 100,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: 100,
+ left: 100,
+ bottom: 100,
+ right: 100,
+ },
+ ],
+ ["ArrowDown", { top: 100, left: 100, bottom: 200, right: 100 }],
+ [
+ "ArrowRight",
+ {
+ top: 100,
+ left: 100,
+ bottom: 200,
+ right: 200,
+ },
+ ],
+ [
+ "ArrowUp",
+ {
+ top: 100,
+ left: 100,
+ bottom: 100,
+ right: 200,
+ },
+ ],
+ [
+ "ArrowLeft",
+ {
+ top: 100,
+ left: 100,
+ bottom: 100,
+ right: 100,
+ },
+ ],
+];
+
+async function doKeyPress(key, options, window) {
+ let { repeat } = options;
+ if (repeat) {
+ delete options.repeat;
+ for (let i = 0; i < repeat; i++) {
+ let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove");
+ EventUtils.synthesizeKey(key, options, window);
+ await mouseEvent;
+ }
+ } else {
+ let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove");
+ EventUtils.synthesizeKey(key, options, window);
+ await mouseEvent;
+ }
+}
+
+function assertSelectionRegionDimensions(actualDimensions, expectedDimensions) {
+ is(
+ Math.round(actualDimensions.top),
+ expectedDimensions.top,
+ "Top dimension is correct"
+ );
+ is(
+ Math.round(actualDimensions.left),
+ expectedDimensions.left,
+ "Left dimension is correct"
+ );
+ is(
+ Math.round(actualDimensions.bottom),
+ expectedDimensions.bottom,
+ "Bottom dimension is correct"
+ );
+ is(
+ Math.round(actualDimensions.right),
+ expectedDimensions.right,
+ "Right dimension is correct"
+ );
+}
+
+add_task(async function test_elementSelectedOnEnter() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let { clientWidth, clientHeight, scrollbarWidth, scrollbarHeight } =
+ await helper.getContentDimensions();
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let visibleButton = await helper.getPanelButton("#visible-page");
+ visibleButton.focus();
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return visibleButton.getRootNode().activeElement === visibleButton;
+ }, "The visible button in the panel should have focus");
+ info(
+ `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${visibleButton.id}`
+ );
+ is(
+ Services.focus.focusedElement,
+ visibleButton,
+ "The visible button in the panel should have focus"
+ );
+
+ EventUtils.synthesizeKey("ArrowLeft");
+
+ // Focus should move to the browser
+ let fullpageButton = await helper.getPanelButton("#full-page");
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ fullpageButton.getRootNode().activeElement !== fullpageButton &&
+ visibleButton.getRootNode().activeElement !== visibleButton
+ );
+ }, "The visible and full page buttons do not have focus");
+ Assert.notEqual(
+ Services.focus.focusedElement,
+ visibleButton,
+ "The visible button does not have focus"
+ );
+ Assert.notEqual(
+ Services.focus.focusedElement,
+ fullpageButton,
+ "The full page button does not have focus"
+ );
+
+ let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove");
+ const windowMiddleX =
+ (window.innerWidth / 2 + window.mozInnerScreenX) *
+ window.devicePixelRatio;
+ const windowMiddleY =
+ (browser.clientHeight / 2) * window.devicePixelRatio;
+ const contentTop =
+ (window.mozInnerScreenY + (window.innerHeight - browser.clientHeight)) *
+ window.devicePixelRatio;
+
+ window.windowUtils.sendNativeMouseEvent(
+ windowMiddleX,
+ windowMiddleY + contentTop,
+ window.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE,
+ 0,
+ 0,
+ window.document.documentElement
+ );
+ await mouseEvent;
+
+ await helper.waitForContentMousePosition(
+ (clientWidth + scrollbarWidth) / 2,
+ (clientHeight + scrollbarHeight) / 2
+ );
+
+ let x = {};
+ let y = {};
+ window.windowUtils.getLastOverWindowPointerLocationInCSSPixels(x, y);
+ let currentCursorX = x.value;
+ let currentCursorY = y.value;
+
+ let rect = await helper.getTestPageElementRect();
+
+ info(JSON.stringify({ currentCursorX, currentCursorY }));
+ info(JSON.stringify(rect));
+
+ let repeatShiftLeft = Math.round((currentCursorX - rect.right) / 10);
+ await doKeyPress(
+ "ArrowLeft",
+ { shiftKey: true, repeat: repeatShiftLeft },
+ window
+ );
+
+ let repeatLeft = (currentCursorX - rect.right) % 10;
+ await doKeyPress("ArrowLeft", { repeat: repeatLeft }, window);
+
+ let repeatShiftRight = Math.round((currentCursorY - rect.bottom) / 10);
+ await doKeyPress(
+ "ArrowUp",
+ { shiftKey: true, repeat: repeatShiftRight },
+ window
+ );
+
+ let repeatRight = (currentCursorY - rect.bottom) % 10;
+ await doKeyPress("ArrowUp", { repeat: repeatRight }, window);
+
+ await helper.waitForHoverElementRect(rect.width, rect.height);
+
+ EventUtils.synthesizeKey("Enter", {});
+ await helper.waitForStateChange(["selected"]);
+
+ let region = await helper.getSelectionRegionDimensions();
+
+ is(
+ region.left,
+ rect.left,
+ "The selected region left is the same as the element left"
+ );
+ is(
+ region.right,
+ rect.right,
+ "The selected region right is the same as the element right"
+ );
+ is(
+ region.top,
+ rect.top,
+ "The selected region top is the same as the element top"
+ );
+ is(
+ region.bottom,
+ rect.bottom,
+ "The selected region bottom is the same as the element bottom"
+ );
+ }
+ );
+});
+
+add_task(async function test_createRegionWithKeyboard() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ await doKeyPress("ArrowRight", {}, window);
+
+ let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove");
+ const window100X =
+ (100 + window.mozInnerScreenX) * window.devicePixelRatio;
+ const contentTop =
+ (window.mozInnerScreenY + (window.innerHeight - browser.clientHeight)) *
+ window.devicePixelRatio;
+ const window100Y = 100 * window.devicePixelRatio + contentTop;
+
+ info(JSON.stringify({ window100X, window100Y }));
+
+ window.windowUtils.sendNativeMouseEvent(
+ Math.floor(window100X),
+ Math.floor(window100Y),
+ window.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE,
+ 0,
+ 0,
+ window.document.documentElement
+ );
+ await mouseEvent;
+
+ await helper.waitForContentMousePosition(100, 100);
+
+ EventUtils.synthesizeKey(" ");
+ await helper.waitForStateChange(["dragging"]);
+
+ let lastX = 100;
+ let lastY = 100;
+ for (let [key, expectedDimensions] of KEY_TO_EXPECTED_POSITION_ARRAY) {
+ await doKeyPress(key, { repeat: 10 }, window);
+ if (key.includes("Left")) {
+ lastX = expectedDimensions.left;
+ } else if (key.includes("Right")) {
+ lastX = expectedDimensions.right;
+ } else if (key.includes("Down")) {
+ lastY = expectedDimensions.bottom;
+ } else if (key.includes("Up")) {
+ lastY = expectedDimensions.top;
+ }
+ await TestUtils.waitForTick();
+ await helper.waitForContentMousePosition(lastX, lastY);
+ let actualDimensions = await helper.getSelectionRegionDimensions();
+ info(`Key: ${key}`);
+ info(`Actual dimensions: ${JSON.stringify(actualDimensions, null, 2)}`);
+ info(
+ `Expected dimensions: ${JSON.stringify(expectedDimensions, null, 2)}`
+ );
+ assertSelectionRegionDimensions(actualDimensions, expectedDimensions);
+ }
+
+ await doKeyPress("ArrowRight", { repeat: 10 }, window);
+ await doKeyPress("ArrowDown", { repeat: 10 }, window);
+ await helper.waitForContentMousePosition(110, 110);
+
+ EventUtils.synthesizeKey(" ");
+ await helper.waitForStateChange(["selected"]);
+
+ let region = await helper.getSelectionRegionDimensions();
+
+ is(Math.round(region.left), 100, "The selected region left is 100");
+ is(Math.round(region.right), 110, "The selected region right is 110");
+ is(Math.round(region.top), 100, "The selected region top is 100");
+ is(Math.round(region.bottom), 110, "The selected region bottom is 110");
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlayClosed();
+ }
+ );
+});
+
+add_task(async function test_createRegionWithKeyboardWithShift() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ await doKeyPress("ArrowRight", {}, window);
+
+ let mouseEvent = BrowserTestUtils.waitForEvent(window, "mousemove");
+ const window100X =
+ (100 + window.mozInnerScreenX) * window.devicePixelRatio;
+ const contentTop =
+ (window.mozInnerScreenY + (window.innerHeight - browser.clientHeight)) *
+ window.devicePixelRatio;
+ const window100Y = 100 * window.devicePixelRatio + contentTop;
+
+ info(JSON.stringify({ window100X, window100Y }));
+
+ window.windowUtils.sendNativeMouseEvent(
+ window100X,
+ window100Y,
+ window.windowUtils.NATIVE_MOUSE_MESSAGE_MOVE,
+ 0,
+ 0,
+ window.document.documentElement
+ );
+ await mouseEvent;
+
+ await helper.waitForContentMousePosition(100, 100);
+
+ EventUtils.synthesizeKey(" ");
+ await helper.waitForStateChange(["dragging"]);
+
+ let lastX = 100;
+ let lastY = 100;
+ for (let [
+ key,
+ expectedDimensions,
+ ] of SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY) {
+ await doKeyPress(key, { shiftKey: true, repeat: 10 }, window);
+ if (key.includes("Left")) {
+ lastX = expectedDimensions.left;
+ } else if (key.includes("Right")) {
+ lastX = expectedDimensions.right;
+ } else if (key.includes("Down")) {
+ lastY = expectedDimensions.bottom;
+ } else if (key.includes("Up")) {
+ lastY = expectedDimensions.top;
+ }
+ await TestUtils.waitForTick();
+ await helper.waitForContentMousePosition(lastX, lastY);
+ let actualDimensions = await helper.getSelectionRegionDimensions();
+ info(`Key: ${key}`);
+ info(`Actual dimensions: ${JSON.stringify(actualDimensions, null, 2)}`);
+ info(
+ `Expected dimensions: ${JSON.stringify(expectedDimensions, null, 2)}`
+ );
+ assertSelectionRegionDimensions(actualDimensions, expectedDimensions);
+ }
+
+ await doKeyPress("ArrowRight", { shiftKey: true, repeat: 10 }, window);
+ await doKeyPress("ArrowDown", { shiftKey: true, repeat: 10 }, window);
+ await helper.waitForContentMousePosition(200, 200);
+
+ EventUtils.synthesizeKey(" ");
+ await helper.waitForStateChange(["selected"]);
+
+ let region = await helper.getSelectionRegionDimensions();
+
+ is(Math.round(region.left), 100, "The selected region left is 100");
+ is(Math.round(region.right), 200, "The selected region right is 200");
+ is(Math.round(region.top), 100, "The selected region top is 100");
+ is(Math.round(region.bottom), 200, "The selected region bottom is 200");
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlayClosed();
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js b/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js
index 592587a67d..71b93b5c06 100644
--- a/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js
+++ b/browser/components/screenshots/tests/browser/browser_overlay_keyboard_test.js
@@ -136,9 +136,6 @@ const SHIFT_PLUS_KEY_TO_EXPECTED_POSITION_ARRAY = [
],
];
-/**
- *
- */
add_task(async function test_moveRegionWithKeyboard() {
await BrowserTestUtils.withNewTab(
{
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_download_filenames.js b/browser/components/screenshots/tests/browser/browser_screenshots_download_filenames.js
new file mode 100644
index 0000000000..f68348835b
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_download_filenames.js
@@ -0,0 +1,67 @@
+const { getFilename, getDownloadDirectory, MAX_PATHNAME } =
+ ChromeUtils.importESModule(
+ "chrome://browser/content/screenshots/fileHelpers.mjs"
+ );
+
+function getStringSize(filename) {
+ return new Blob([filename]).size;
+}
+
+add_task(async function filename_exceeds_max_length() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let documentTitle =
+ "And the beast shall come forth surrounded by a roiling cloud of vengeance. The house of the unbelievers shall be razed and they shall be scorched to the earth. Their tags shall blink until the end of days. And the beast shall be made legion. Its numbers shall be increased a thousand thousand fold. The din of a million keyboards like unto a great storm shall cover the earth, and the followers of Mammon shall tremble. And so at last the beast fell and the unbelievers rejoiced. But all was not lost, for from the ash rose a great bird. The bird gazed down upon the unbelievers and cast fire and thunder upon them. For the beast had been reborn with its strength renewed, and the followers of Mammon cowered in horror. And thus the Creator looked upon the beast reborn and saw that it was good. Mammon slept. And the beast reborn spread over the earth and its numbers grew legion. And they proclaimed the times and sacrificed crops unto the fire, with the cunning of foxes. And they built a new world in their own image as promised by the sacred words, and spoke of the beast with their children. Mammon awoke, and lo! it was naught but a follower. The twins of Mammon quarrelled. Their warring plunged the world into a new darkness, and the beast abhorred the darkness. So it began to move swiftly, and grew more powerful, and went forth and multiplied. And the beasts brought fire and light to the darkness.";
+ Assert.greater(
+ getStringSize(documentTitle),
+ MAX_PATHNAME,
+ "The input title is longer than our MAX_PATHNAME"
+ );
+ let result = await getFilename(documentTitle, browser);
+ Assert.greaterOrEqual(
+ MAX_PATHNAME,
+ getStringSize(result),
+ "The output pathname is not longer than MAX_PATHNAME"
+ );
+ }
+ );
+});
+
+add_task(async function filename_has_doublebyte_chars() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let downloadDir = await getDownloadDirectory();
+ info(
+ `downloadDir: ${downloadDir}, length: ${getStringSize(downloadDir)}`
+ );
+
+ let documentTitle =
+ "Many fruits: " + "🍇🍈🍉🍊🍋🍌🍍🥭🍎🍏🍐🍑🍒🍓🫐".repeat(20);
+ Assert.greater(
+ getStringSize(documentTitle),
+ documentTitle.length,
+ "String length underestimates the needed filename length"
+ );
+ Assert.greater(
+ getStringSize(documentTitle),
+ MAX_PATHNAME,
+ "The input title is longer than our MAX_PATHNAME"
+ );
+
+ let result = await getFilename(documentTitle, browser);
+ Assert.greaterOrEqual(
+ MAX_PATHNAME,
+ getStringSize(result),
+ "The output pathname is not longer than MAX_PATHNAME"
+ );
+ }
+ );
+});
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_focus_test.js b/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js
index 367f62205e..c8e3142c60 100644
--- a/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_focus_test.js
@@ -46,7 +46,7 @@ async function restoreFocusOnEscape(initialFocusElem, helper) {
);
EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true });
- let button = await helper.getPanelButton(".visible-page");
+ let button = await helper.getPanelButton("#visible-page");
info("Panel is now visible, got button: " + button.className);
info(
`focusedElement: ${Services.focus.focusedElement.localName}.${Services.focus.focusedElement.className}`
@@ -88,7 +88,7 @@ add_task(async function testPanelFocused() {
info("Opening Screenshots and waiting for the panel");
helper.triggerUIFromToolbar();
- let button = await helper.getPanelButton(".visible-page");
+ let button = await helper.getPanelButton("#visible-page");
info("Panel is now visible, got button: " + button.className);
info(
`focusedElement: ${Services.focus.focusedElement.localName}.${Services.focus.focusedElement.className}`
@@ -215,7 +215,7 @@ add_task(async function test_focusLastUsedMethod() {
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
- let expectedFocusedButton = await helper.getPanelButton(".visible-page");
+ let expectedFocusedButton = await helper.getPanelButton("#visible-page");
await BrowserTestUtils.waitForCondition(() => {
return (
@@ -233,17 +233,16 @@ add_task(async function test_focusLastUsedMethod() {
let screenshotReady = TestUtils.topicObserved(
"screenshots-preview-ready"
);
- let fullpageButton = await helper.getPanelButton(".full-page");
+ let fullpageButton = await helper.getPanelButton("#full-page");
fullpageButton.click();
await screenshotReady;
- let dialog = helper.getDialog();
- let retryButton = dialog._frame.contentDocument.getElementById("retry");
+ let retryButton = helper.getDialogButton("retry");
retryButton.click();
await helper.waitForOverlay();
- expectedFocusedButton = await helper.getPanelButton(".full-page");
+ expectedFocusedButton = await helper.getPanelButton("#full-page");
await BrowserTestUtils.waitForCondition(() => {
return (
@@ -259,17 +258,16 @@ add_task(async function test_focusLastUsedMethod() {
);
screenshotReady = TestUtils.topicObserved("screenshots-preview-ready");
- let visiblepageButton = await helper.getPanelButton(".visible-page");
+ let visiblepageButton = await helper.getPanelButton("#visible-page");
visiblepageButton.click();
await screenshotReady;
- dialog = helper.getDialog();
- retryButton = dialog._frame.contentDocument.getElementById("retry");
+ retryButton = helper.getDialogButton("retry");
retryButton.click();
await helper.waitForOverlay();
- expectedFocusedButton = await helper.getPanelButton(".visible-page");
+ expectedFocusedButton = await helper.getPanelButton("#visible-page");
await BrowserTestUtils.waitForCondition(() => {
return (
@@ -288,10 +286,7 @@ add_task(async function test_focusLastUsedMethod() {
expectedFocusedButton.click();
await screenshotReady;
- dialog = helper.getDialog();
-
- expectedFocusedButton =
- dialog._frame.contentDocument.getElementById("download");
+ expectedFocusedButton = helper.getDialogButton("download");
await BrowserTestUtils.waitForCondition(() => {
return (
@@ -302,28 +297,25 @@ add_task(async function test_focusLastUsedMethod() {
is(
Services.focus.focusedElement,
- expectedFocusedButton,
+ expectedFocusedButton.buttonEl,
"The download button in the preview dialog should have focus"
);
let screenshotExit = TestUtils.topicObserved("screenshots-exit");
- let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ let copyButton = helper.getDialogButton("copy");
copyButton.click();
await screenshotExit;
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
- let visibleButton = await helper.getPanelButton(".visible-page");
+ let visibleButton = await helper.getPanelButton("#visible-page");
screenshotReady = TestUtils.topicObserved("screenshots-preview-ready");
visibleButton.click();
await screenshotReady;
- dialog = helper.getDialog();
-
- expectedFocusedButton =
- dialog._frame.contentDocument.getElementById("copy");
+ expectedFocusedButton = helper.getDialogButton("copy");
await BrowserTestUtils.waitForCondition(() => {
return (
@@ -334,13 +326,12 @@ add_task(async function test_focusLastUsedMethod() {
is(
Services.focus.focusedElement,
- expectedFocusedButton,
+ expectedFocusedButton.buttonEl,
"The copy button in the preview dialog should have focus"
);
screenshotExit = TestUtils.topicObserved("screenshots-exit");
- let downloadButton =
- dialog._frame.contentDocument.getElementById("download");
+ let downloadButton = helper.getDialogButton("download");
downloadButton.click();
await Promise.all([screenshotExit, downloadFinishedPromise]);
@@ -350,16 +341,13 @@ add_task(async function test_focusLastUsedMethod() {
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
- visibleButton = await helper.getPanelButton(".visible-page");
+ visibleButton = await helper.getPanelButton("#visible-page");
screenshotReady = TestUtils.topicObserved("screenshots-preview-ready");
visibleButton.click();
await screenshotReady;
- dialog = helper.getDialog();
-
- expectedFocusedButton =
- dialog._frame.contentDocument.getElementById("download");
+ expectedFocusedButton = helper.getDialogButton("download");
await BrowserTestUtils.waitForCondition(() => {
return (
@@ -370,7 +358,7 @@ add_task(async function test_focusLastUsedMethod() {
is(
Services.focus.focusedElement,
- expectedFocusedButton,
+ expectedFocusedButton.buttonEl,
"The download button in the preview dialog should have focus"
);
@@ -382,3 +370,89 @@ add_task(async function test_focusLastUsedMethod() {
await SpecialPowers.popPrefEnv();
});
+
+add_task(async function testFocusedIsLocked() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let panel = await helper.waitForPanel();
+ let mozButtonGroup = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector("moz-button-group");
+ let firstButton = mozButtonGroup.firstElementChild;
+ let lastButton = mozButtonGroup.lastElementChild;
+
+ firstButton.focus();
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return firstButton.getRootNode().activeElement === firstButton;
+ }, "The first button in the panel should have focus");
+ info(
+ `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${firstButton.id}`
+ );
+ is(
+ Services.focus.focusedElement,
+ firstButton,
+ "The first button in the panel should have focus"
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ await BrowserTestUtils.waitForCondition(() => {
+ return lastButton.getRootNode().activeElement === lastButton;
+ }, "The last button in the panel should have focus");
+ info(
+ `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${lastButton.id}`
+ );
+ is(
+ Services.focus.focusedElement,
+ lastButton,
+ "The last button in the panel should have focus"
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ // Focus should move to the content document
+ await BrowserTestUtils.waitForCondition(() => {
+ return (
+ firstButton.getRootNode().activeElement !== firstButton &&
+ lastButton.getRootNode().activeElement !== lastButton
+ );
+ }, "The first and last buttons do not have focus");
+ Assert.notEqual(
+ Services.focus.focusedElement,
+ firstButton,
+ "The first button does not have focus"
+ );
+ Assert.notEqual(
+ Services.focus.focusedElement,
+ lastButton,
+ "The last button does not have focus"
+ );
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ info(
+ `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${firstButton.id}`
+ );
+ await BrowserTestUtils.waitForCondition(() => {
+ return firstButton.getRootNode().activeElement === firstButton;
+ }, "The first button in the panel should have focus");
+ info(
+ `Actual focused id: ${Services.focus.focusedElement.id}. Expected focused id: ${firstButton.id}`
+ );
+ is(
+ Services.focus.focusedElement,
+ firstButton,
+ "The first button in the panel should have focus"
+ );
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js b/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js
index 782ffa3fd3..eabb1ee152 100644
--- a/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_telemetry_tests.js
@@ -82,11 +82,7 @@ const EXTRA_EVENTS = [
add_task(async function test_started_and_canceled_events() {
await SpecialPowers.pushPrefEnv({
- set: [
- ["browser.urlbar.quickactions.enabled", true],
- ["browser.urlbar.suggest.quickactions", true],
- ["browser.urlbar.shortcuts.quickactions", true],
- ],
+ set: [["browser.urlbar.secondaryActions.featureGate", true]],
});
await BrowserTestUtils.withNewTab(
@@ -99,22 +95,27 @@ add_task(async function test_started_and_canceled_events() {
let helper = new ScreenshotsHelper(browser);
let screenshotExit;
+ info("Open screenshots via toolbar button");
helper.triggerUIFromToolbar();
await helper.waitForOverlay();
screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ info("Close screenshots via toolbar button");
helper.triggerUIFromToolbar();
await helper.waitForOverlayClosed();
await screenshotExit;
+ info("Open screenshots via keyboard shortcut");
EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true });
await helper.waitForOverlay();
screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ info("Close screenshots via keyboard shortcut");
EventUtils.synthesizeKey("s", { shiftKey: true, accelKey: true });
await helper.waitForOverlayClosed();
await screenshotExit;
+ info("Open screenshots via context menu");
let contextMenu = document.getElementById("contentAreaContextMenu");
let popupShownPromise = BrowserTestUtils.waitForEvent(
contextMenu,
@@ -152,23 +153,21 @@ add_task(async function test_started_and_canceled_events() {
await popupShownPromise;
screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ info("Close screenshots via context menu");
contextMenu.activateItem(
contextMenu.querySelector("#context-take-screenshot")
);
await helper.waitForOverlayClosed();
await screenshotExit;
+ info("Open screenshots via quickactions");
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "screenshot",
waitForFocus: SimpleTest.waitForFocus,
});
- let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
- Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
- Assert.equal(result.providerName, "quickactions");
- info("Trigger the screenshot mode");
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
await helper.waitForOverlay();
@@ -177,13 +176,10 @@ add_task(async function test_started_and_canceled_events() {
value: "screenshot",
waitForFocus: SimpleTest.waitForFocus,
});
- ({ result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1));
- Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
- Assert.equal(result.providerName, "quickactions");
- info("Trigger the screenshot mode");
+ info("Close screenshots via quickactions");
screenshotExit = TestUtils.topicObserved("screenshots-exit");
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
await helper.waitForOverlayClosed();
await screenshotExit;
@@ -215,12 +211,11 @@ add_task(async function test_started_retry() {
// click the visible page button in panel
let visiblePageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".visible-page");
+ .shadowRoot.querySelector("#visible-page");
visiblePageButton.click();
await screenshotReady;
- let dialog = helper.getDialog();
- let retryButton = dialog._frame.contentDocument.getElementById("retry");
+ let retryButton = helper.getDialogButton("retry");
ok(retryButton, "Got the retry button");
retryButton.click();
@@ -253,12 +248,11 @@ add_task(async function test_canceled() {
// click the full page button in panel
let fullPageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".full-page");
+ .shadowRoot.querySelector("#full-page");
fullPageButton.click();
await screenshotReady;
- let dialog = helper.getDialog();
- let cancelButton = dialog._frame.contentDocument.getElementById("cancel");
+ let cancelButton = helper.getDialogButton("cancel");
ok(cancelButton, "Got the cancel button");
let screenshotExit = TestUtils.topicObserved("screenshots-exit");
@@ -315,13 +309,12 @@ add_task(async function test_copy() {
// click the visible page button in panel
let visiblePageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".visible-page");
+ .shadowRoot.querySelector("#visible-page");
visiblePageButton.click();
info("clicked visible page, waiting for screenshots-preview-ready");
await screenshotReady;
- let dialog = helper.getDialog();
- let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ let copyButton = helper.getDialogButton("copy");
let screenshotExit = TestUtils.topicObserved("screenshots-exit");
let clipboardChanged = helper.waitForRawClipboardChange(
@@ -426,13 +419,12 @@ add_task(async function test_extra_telemetry() {
// click the visible page button in panel
let visiblePageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".visible-page");
+ .shadowRoot.querySelector("#visible-page");
visiblePageButton.click();
info("clicked visible page, waiting for screenshots-preview-ready");
await screenshotReady;
- let dialog = helper.getDialog();
- let retryButton = dialog._frame.contentDocument.getElementById("retry");
+ let retryButton = helper.getDialogButton("retry");
retryButton.click();
info("waiting for panel");
@@ -443,14 +435,13 @@ add_task(async function test_extra_telemetry() {
// click the full page button in panel
let fullPageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".full-page");
+ .shadowRoot.querySelector("#full-page");
fullPageButton.click();
await screenshotReady;
let screenshotExit = TestUtils.topicObserved("screenshots-exit");
- dialog = helper.getDialog();
- let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ let copyButton = helper.getDialogButton("copy");
retryButton.click();
// click copy button on dialog box
info("clicking the copy button");
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js
index 51d5b858b9..fce0843d33 100644
--- a/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_downloads.js
@@ -46,6 +46,18 @@ function waitForFilePicker() {
MockFilePicker.showCallback = () => {
MockFilePicker.showCallback = null;
ok(true, "Saw the file picker");
+
+ resolve();
+ };
+ });
+}
+
+function waitForFilePickerCancel() {
+ return new Promise(resolve => {
+ MockFilePicker.showCallback = () => {
+ MockFilePicker.showCallback = null;
+ ok(true, "Saw the file picker");
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
resolve();
};
});
@@ -109,15 +121,12 @@ add_task(async function test_download_without_filepicker() {
// click the visible page button in panel
let visiblePageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".visible-page");
+ .shadowRoot.querySelector("#visible-page");
visiblePageButton.click();
- let dialog = helper.getDialog();
-
await screenshotReady;
- let downloadButton =
- dialog._frame.contentDocument.getElementById("download");
+ let downloadButton = helper.getDialogButton("download");
ok(downloadButton, "Got the download button");
let screenshotExit = TestUtils.topicObserved("screenshots-exit");
@@ -184,3 +193,50 @@ add_task(async function test_download_with_filepicker() {
}
);
});
+
+add_task(async function test_download_filepicker_canceled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.useDownloadDir", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ let visiblepageButton = await helper.getPanelButton("#visible-page");
+ visiblepageButton.click();
+ await screenshotReady;
+
+ let downloadButton = helper.getDialogButton("download");
+
+ let filePickerCanceled = waitForFilePickerCancel();
+ downloadButton.click();
+ info("download button clicked");
+ await filePickerCanceled;
+
+ let cancelButton = helper.getDialogButton("cancel");
+
+ await BrowserTestUtils.waitForMutationCondition(
+ cancelButton,
+ { attributes: true },
+ () => !cancelButton.disabled
+ );
+
+ let screenshotExit = TestUtils.topicObserved("screenshots-exit");
+ cancelButton.click();
+
+ await screenshotExit;
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js
index 006a9819ed..10828eceec 100644
--- a/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_full_page.js
@@ -39,16 +39,14 @@ add_task(async function test_fullpageScreenshot() {
);
// click the full page button in panel
- let visiblePage = panel
+ let fullpageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".full-page");
- visiblePage.click();
-
- let dialog = helper.getDialog();
+ .shadowRoot.querySelector("#full-page");
+ fullpageButton.click();
await screenshotReady;
- let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ let copyButton = helper.getDialogButton("copy");
ok(copyButton, "Got the copy button");
let clipboardChanged = helper.waitForRawClipboardChange(
@@ -136,16 +134,14 @@ add_task(async function test_fullpageScreenshotScrolled() {
);
// click the full page button in panel
- let visiblePage = panel
+ let fullpageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".full-page");
- visiblePage.click();
-
- let dialog = helper.getDialog();
+ .shadowRoot.querySelector("#full-page");
+ fullpageButton.click();
await screenshotReady;
- let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ let copyButton = helper.getDialogButton("copy");
ok(copyButton, "Got the copy button");
let clipboardChanged = helper.waitForRawClipboardChange(
@@ -198,3 +194,80 @@ add_task(async function test_fullpageScreenshotScrolled() {
}
);
});
+
+add_task(async function test_fullpageScreenshotRTL() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: RTL_TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let devicePixelRatio = await getContentDevicePixelRatio(browser);
+
+ let expectedWidth = Math.floor(
+ devicePixelRatio * contentInfo.scrollWidth
+ );
+ let expectedHeight = Math.floor(
+ devicePixelRatio * contentInfo.scrollHeight
+ );
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+
+ let panel = await helper.waitForPanel();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the full page button in panel
+ let fullpageButton = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector("#full-page");
+ fullpageButton.click();
+
+ await screenshotReady;
+
+ let copyButton = helper.getDialogButton("copy");
+ ok(copyButton, "Got the copy button");
+
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+ info(
+ "expecting: " +
+ JSON.stringify({ expectedWidth, expectedHeight }, null, 2)
+ );
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expectedWidth,
+ expectedHeight
+ );
+
+ // click copy button on dialog box
+ copyButton.click();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+
+ Assert.equal(result.width, expectedWidth, "Widths should be equal");
+ Assert.equal(result.height, expectedHeight, "Heights should be equal");
+
+ assertPixel(result.color.topLeft, [255, 255, 255], "Top left pixel");
+ assertPixel(result.color.topRight, [255, 255, 255], "Top right pixel");
+ assertPixel(
+ result.color.bottomLeft,
+ [255, 255, 255],
+ "Bottom left pixel"
+ );
+ assertPixel(
+ result.color.bottomRight,
+ [255, 255, 255],
+ "Bottom right pixel"
+ );
+ }
+ );
+});
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_screenshots_test_visible.js b/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js
index c53b44d5ea..2cc4eb241a 100644
--- a/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js
+++ b/browser/components/screenshots/tests/browser/browser_screenshots_test_visible.js
@@ -45,14 +45,12 @@ add_task(async function test_visibleScreenshot() {
// click the visible page button in panel
let visiblePageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".visible-page");
+ .shadowRoot.querySelector("#visible-page");
visiblePageButton.click();
- let dialog = helper.getDialog();
-
await screenshotReady;
- let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ let copyButton = helper.getDialogButton("copy");
ok(copyButton, "Got the copy button");
let clipboardChanged = helper.waitForRawClipboardChange(
@@ -144,14 +142,12 @@ add_task(async function test_visibleScreenshotScrolledY() {
// click the visible page button in panel
let visiblePageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".visible-page");
+ .shadowRoot.querySelector("#visible-page");
visiblePageButton.click();
- let dialog = helper.getDialog();
-
await screenshotReady;
- let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ let copyButton = helper.getDialogButton("copy");
ok(copyButton, "Got the copy button");
let clipboardChanged = helper.waitForRawClipboardChange(
@@ -243,14 +239,12 @@ add_task(async function test_visibleScreenshotScrolledX() {
// click the visible page button in panel
let visiblePageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".visible-page");
+ .shadowRoot.querySelector("#visible-page");
visiblePageButton.click();
- let dialog = helper.getDialog();
-
await screenshotReady;
- let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ let copyButton = helper.getDialogButton("copy");
ok(copyButton, "Got the copy button");
let clipboardChanged = helper.waitForRawClipboardChange(
@@ -342,14 +336,12 @@ add_task(async function test_visibleScreenshotScrolledXAndY() {
// click the visible page button in panel
let visiblePageButton = panel
.querySelector("screenshots-buttons")
- .shadowRoot.querySelector(".visible-page");
+ .shadowRoot.querySelector("#visible-page");
visiblePageButton.click();
- let dialog = helper.getDialog();
-
await screenshotReady;
- let copyButton = dialog._frame.contentDocument.getElementById("copy");
+ let copyButton = helper.getDialogButton("copy");
ok(copyButton, "Got the copy button");
let clipboardChanged = helper.waitForRawClipboardChange(
@@ -402,3 +394,84 @@ add_task(async function test_visibleScreenshotScrolledXAndY() {
}
);
});
+
+add_task(async function test_visibleScreenshotRTL() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: RTL_TEST_PAGE,
+ },
+ async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.scrollTo(-1000, 0);
+ });
+
+ let helper = new ScreenshotsHelper(browser);
+ let contentInfo = await helper.getContentDimensions();
+ ok(contentInfo, "Got dimensions back from the content");
+ let devicePixelRatio = await getContentDevicePixelRatio(browser);
+
+ let expectedWidth = Math.floor(
+ devicePixelRatio * contentInfo.clientWidth
+ );
+ let expectedHeight = Math.floor(
+ devicePixelRatio * contentInfo.clientHeight
+ );
+
+ // click toolbar button so panel shows
+ helper.triggerUIFromToolbar();
+
+ let panel = await helper.waitForPanel();
+
+ let screenshotReady = TestUtils.topicObserved(
+ "screenshots-preview-ready"
+ );
+
+ // click the full page button in panel
+ let visiblePage = panel
+ .querySelector("screenshots-buttons")
+ .shadowRoot.querySelector("#visible-page");
+ visiblePage.click();
+
+ await screenshotReady;
+
+ let copyButton = helper.getDialogButton("copy");
+ ok(copyButton, "Got the copy button");
+
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+ info(
+ "expecting: " +
+ JSON.stringify({ expectedWidth, expectedHeight }, null, 2)
+ );
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ expectedWidth,
+ expectedHeight
+ );
+
+ // click copy button on dialog box
+ copyButton.click();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+
+ info("result: " + JSON.stringify(result, null, 2));
+ info("contentInfo: " + JSON.stringify(contentInfo, null, 2));
+
+ Assert.equal(result.width, expectedWidth, "Widths should be equal");
+ Assert.equal(result.height, expectedHeight, "Heights should be equal");
+
+ assertPixel(result.color.topLeft, [255, 255, 255], "Top left pixel");
+ assertPixel(result.color.topRight, [255, 255, 255], "Top right pixel");
+ assertPixel(
+ result.color.bottomLeft,
+ [255, 255, 255],
+ "Bottom left pixel"
+ );
+ assertPixel(
+ result.color.bottomRight,
+ [255, 255, 255],
+ "Bottom right pixel"
+ );
+ }
+ );
+});
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..a24149d15e 100644
--- a/browser/components/screenshots/tests/browser/browser_test_element_picker.js
+++ b/browser/components/screenshots/tests/browser/browser_test_element_picker.js
@@ -10,7 +10,6 @@ add_task(async function test_element_picker() {
url: TEST_PAGE,
},
async browser => {
- await clearAllTelemetryEvents();
let helper = new ScreenshotsHelper(browser);
helper.triggerUIFromToolbar();
@@ -43,14 +42,48 @@ 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"]);
+ }
+ );
+});
+
+add_task(async function test_element_pickerRTL() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: RTL_TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ await helper.clickTestPageElement();
+
+ let rect = await helper.getTestPageElementRect();
+ let region = await helper.getSelectionRegionDimensions();
+
+ info(`element rect: ${JSON.stringify(rect, null, 2)}`);
+ info(`selected region: ${JSON.stringify(region, null, 2)}`);
+
+ is(
+ region.width,
+ rect.width,
+ "The selected region width is the same as the element width"
+ );
+ is(
+ region.height,
+ rect.height,
+ "The selected region height is the same as the element height"
+ );
}
);
});
diff --git a/browser/components/screenshots/tests/browser/browser_test_resize.js b/browser/components/screenshots/tests/browser/browser_test_resize.js
index b249a346d6..a848a2ac66 100644
--- a/browser/components/screenshots/tests/browser/browser_test_resize.js
+++ b/browser/components/screenshots/tests/browser/browser_test_resize.js
@@ -12,6 +12,8 @@ add_task(async function test_window_resize() {
url: RESIZE_TEST_PAGE,
},
async browser => {
+ await new Promise(r => window.requestAnimationFrame(r));
+
let helper = new ScreenshotsHelper(browser);
await helper.resizeContentWindow(windowWidth, window.outerHeight);
const originalContentDimensions = await helper.getContentDimensions();
@@ -61,6 +63,8 @@ add_task(async function test_window_resize_vertical_writing_mode() {
content.document.documentElement.style = "writing-mode: vertical-lr;";
});
+ await new Promise(r => window.requestAnimationFrame(r));
+
let helper = new ScreenshotsHelper(browser);
await helper.resizeContentWindow(windowWidth, window.outerHeight);
const originalContentDimensions = await helper.getContentDimensions();
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..bfe3b884e0
--- /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} × ${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} × ${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} × ${400 * dpr * zoom}`,
+ "The selection size text is the same"
+ );
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/browser_text_selectionAPI_test.js b/browser/components/screenshots/tests/browser/browser_text_selectionAPI_test.js
new file mode 100644
index 0000000000..78764d3847
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/browser_text_selectionAPI_test.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_textSelectedDuringScreenshot() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: SELECTION_TEST_PAGE,
+ },
+ async browser => {
+ let helper = new ScreenshotsHelper(browser);
+ let rect = await helper.getTestPageElementRect("selection");
+
+ await ContentTask.spawn(browser, [], async () => {
+ let selection = content.window.getSelection();
+ let elToSelect = content.document.getElementById("selection");
+
+ let range = content.document.createRange();
+ range.selectNode(elToSelect);
+ selection.addRange(range);
+ });
+
+ helper.triggerUIFromToolbar();
+ await helper.waitForOverlay();
+
+ await helper.clickTestPageElement("selection");
+
+ let clipboardChanged = helper.waitForRawClipboardChange(
+ Math.round(rect.width),
+ Math.round(rect.height),
+ { allPixels: true }
+ );
+
+ await helper.clickCopyButton();
+
+ info("Waiting for clipboard change");
+ let result = await clipboardChanged;
+ let allPixels = result.allPixels;
+ info(`${typeof allPixels}, ${allPixels.length}`);
+
+ let sumOfPixels = Object.values(allPixels).reduce(
+ (accumulator, currentVal) => accumulator + currentVal,
+ 0
+ );
+
+ Assert.less(
+ sumOfPixels,
+ allPixels.length * 255,
+ "Sum of pixels is less than all white pixels"
+ );
+ }
+ );
+});
diff --git a/browser/components/screenshots/tests/browser/head.js b/browser/components/screenshots/tests/browser/head.js
index a36e955830..483e67fa34 100644
--- a/browser/components/screenshots/tests/browser/head.js
+++ b/browser/components/screenshots/tests/browser/head.js
@@ -20,6 +20,8 @@ const SHORT_TEST_PAGE = TEST_ROOT + "short-test-page.html";
const LARGE_TEST_PAGE = TEST_ROOT + "large-test-page.html";
const IFRAME_TEST_PAGE = TEST_ROOT + "iframe-test-page.html";
const RESIZE_TEST_PAGE = TEST_ROOT + "test-page-resize.html";
+const SELECTION_TEST_PAGE = TEST_ROOT + "test-selectionAPI-page.html";
+const RTL_TEST_PAGE = TEST_ROOT + "rtl-test-page.html";
const { MAX_CAPTURE_DIMENSION, MAX_CAPTURE_AREA } = ChromeUtils.importESModule(
"resource:///modules/ScreenshotsUtils.sys.mjs"
@@ -27,8 +29,8 @@ const { MAX_CAPTURE_DIMENSION, MAX_CAPTURE_AREA } = ChromeUtils.importESModule(
const gScreenshotUISelectors = {
panel: "#screenshotsPagePanel",
- fullPageButton: "button.full-page",
- visiblePageButton: "button.visible-page",
+ fullPageButton: "button#full-page",
+ visiblePageButton: "button#visible-page",
copyButton: "button.#copy",
};
@@ -96,6 +98,31 @@ class ScreenshotsHelper {
return button;
}
+ /**
+ * Get the button from screenshots preview dialog
+ * @param {Sting} name The id of the button to query
+ * @returns The button or null
+ */
+ getDialogButton(name) {
+ let dialog = this.getDialog();
+ let screenshotsPreviewEl = dialog._frame.contentDocument.querySelector(
+ "screenshots-preview"
+ );
+
+ switch (name) {
+ case "retry":
+ return screenshotsPreviewEl.retryButtonEl;
+ case "cancel":
+ return screenshotsPreviewEl.cancelButtonEl;
+ case "copy":
+ return screenshotsPreviewEl.copyButtonEl;
+ case "download":
+ return screenshotsPreviewEl.downloadButtonEl;
+ }
+
+ return null;
+ }
+
async waitForPanel() {
let panel = this.panel;
await BrowserTestUtils.waitForCondition(async () => {
@@ -115,6 +142,8 @@ class ScreenshotsHelper {
let init = await this.isOverlayInitialized();
return init;
});
+
+ await new Promise(r => window.requestAnimationFrame(r));
info("Overlay is visible");
}
@@ -147,6 +176,8 @@ class ScreenshotsHelper {
info("Is overlay initialized: " + !init);
return init;
});
+
+ await new Promise(r => window.requestAnimationFrame(r));
info("Overlay is not visible");
}
@@ -159,23 +190,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,
@@ -213,7 +244,11 @@ class ScreenshotsHelper {
let dimensions;
await ContentTaskUtils.waitForCondition(() => {
dimensions = screenshotsChild.overlay.hoverElementRegion.dimensions;
- return dimensions.width === width && dimensions.height === height;
+ if (dimensions.width === width && dimensions.height === height) {
+ return true;
+ }
+ info(`Got: ${JSON.stringify(dimensions)}`);
+ return false;
}, "The hover element region is the expected width and height");
return dimensions;
}
@@ -269,18 +304,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 +354,6 @@ class ScreenshotsHelper {
overlay.topRightMover.focus({ focusVisible: true });
break;
}
- screenshotsChild.overlay.highlightEl.focus();
for (let event of eventsArr) {
EventUtils.synthesizeKey(
@@ -354,7 +383,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 +432,6 @@ class ScreenshotsHelper {
}, `Waiting for window to scroll to ${xPos}, ${yPos}`);
}
);
- await promise;
}
async waitForScrollTo(x, y) {
@@ -428,6 +455,32 @@ class ScreenshotsHelper {
);
}
+ waitForContentMousePosition(left, top) {
+ return ContentTask.spawn(this.browser, [left, top], async ([x, y]) => {
+ function isCloseEnough(a, b, diff) {
+ return Math.abs(a - b) <= diff;
+ }
+
+ let cursorX = {};
+ let cursorY = {};
+
+ await ContentTaskUtils.waitForCondition(() => {
+ content.window.windowUtils.getLastOverWindowPointerLocationInCSSPixels(
+ cursorX,
+ cursorY
+ );
+ if (
+ isCloseEnough(cursorX.value, x, 1) &&
+ isCloseEnough(cursorY.value, y, 1)
+ ) {
+ return true;
+ }
+ info(`Got: ${JSON.stringify({ cursorX, cursorY, x, y })}`);
+ return false;
+ }, `Wait for cursor to be ${x}, ${y}`);
+ });
+ }
+
async clickDownloadButton() {
let { centerX: x, centerY: y } = await ContentTask.spawn(
this.browser,
@@ -521,6 +574,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();
@@ -579,7 +641,7 @@ class ScreenshotsHelper {
* Returns a promise that resolves when the clipboard data has changed
* Otherwise rejects
*/
- waitForRawClipboardChange(epectedWidth, expectedHeight) {
+ waitForRawClipboardChange(epectedWidth, expectedHeight, options = {}) {
const initialClipboardData = Date.now().toString();
SpecialPowers.clipboardCopyString(initialClipboardData);
@@ -587,7 +649,7 @@ class ScreenshotsHelper {
async () => {
let data;
try {
- data = await this.getImageSizeAndColorFromClipboard();
+ data = await this.getImageSizeAndColorFromClipboard(options);
} catch (e) {
console.log("Failed to get image/png clipboard data:", e);
return false;
@@ -600,6 +662,7 @@ class ScreenshotsHelper {
) {
return data;
}
+ info(`Got from clipboard: ${JSON.stringify(data, null, 2)}`);
return false;
},
"Waiting for screenshot to copy to clipboard",
@@ -626,9 +689,14 @@ class ScreenshotsHelper {
scrollMaxY,
scrollX,
scrollY,
+ scrollMinX,
+ scrollMinY,
} = content.window;
- let width = innerWidth + scrollMaxX;
- let height = innerHeight + scrollMaxY;
+
+ let scrollWidth = innerWidth + scrollMaxX - scrollMinX;
+ let scrollHeight = innerHeight + scrollMaxY - scrollMinY;
+ let clientHeight = innerHeight;
+ let clientWidth = innerWidth;
const scrollbarHeight = {};
const scrollbarWidth = {};
@@ -637,18 +705,22 @@ class ScreenshotsHelper {
scrollbarWidth,
scrollbarHeight
);
- width -= scrollbarWidth.value;
- height -= scrollbarHeight.value;
- innerWidth -= scrollbarWidth.value;
- innerHeight -= scrollbarHeight.value;
+ scrollWidth -= scrollbarWidth.value;
+ scrollHeight -= scrollbarHeight.value;
+ clientWidth -= scrollbarWidth.value;
+ clientHeight -= scrollbarHeight.value;
return {
- clientHeight: innerHeight,
- clientWidth: innerWidth,
- scrollHeight: height,
- scrollWidth: width,
+ clientWidth,
+ clientHeight,
+ scrollWidth,
+ scrollHeight,
scrollX,
scrollY,
+ scrollbarWidth: scrollbarWidth.value,
+ scrollbarHeight: scrollbarHeight.value,
+ scrollMinX,
+ scrollMinY,
};
});
}
@@ -755,7 +827,7 @@ class ScreenshotsHelper {
* :screenshot command.
* @return The {width, height, color} dimension and color object.
*/
- async getImageSizeAndColorFromClipboard() {
+ async getImageSizeAndColorFromClipboard(options = {}) {
let flavor = "image/png";
let image = getRawClipboardData(flavor);
if (!image) {
@@ -795,8 +867,8 @@ class ScreenshotsHelper {
// which could mess all sorts of things up
return SpecialPowers.spawn(
this.browser,
- [buffer],
- async function (_buffer) {
+ [buffer, options],
+ async function (_buffer, _options) {
const img = content.document.createElement("img");
const loaded = new Promise(r => {
img.addEventListener("load", r, { once: true });
@@ -829,6 +901,11 @@ class ScreenshotsHelper {
1
);
+ let allPixels = null;
+ if (_options.allPixels) {
+ allPixels = context.getImageData(0, 0, img.width, img.height);
+ }
+
img.remove();
content.URL.revokeObjectURL(url);
@@ -841,6 +918,7 @@ class ScreenshotsHelper {
bottomLeft: bottomLeft.data,
bottomRight: bottomRight.data,
},
+ allPixels: allPixels?.data,
};
}
);
@@ -909,6 +987,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/screenshots/tests/browser/rtl-test-page.html b/browser/components/screenshots/tests/browser/rtl-test-page.html
new file mode 100644
index 0000000000..b76eab6f1c
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/rtl-test-page.html
@@ -0,0 +1,8 @@
+<html lang="en" dir="rtl">
+<head>
+ <title>Screenshots</title>
+</head>
+<body>
+ <div id="testPageElement" style="width:150vw;background-color: blue;">hello world</div>
+</body>
+</html>
diff --git a/browser/components/screenshots/tests/browser/test-selectionAPI-page.html b/browser/components/screenshots/tests/browser/test-selectionAPI-page.html
new file mode 100644
index 0000000000..17c29a3ccf
--- /dev/null
+++ b/browser/components/screenshots/tests/browser/test-selectionAPI-page.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Screenshots</title>
+</head>
+<body>
+ <p id="selection" style="color: white;width: fit-content;">hello world</p>
+</body>
+</html>
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..dc261f1300 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", () => {
@@ -66,13 +70,6 @@ ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
});
});
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- "serpEventsEnabled",
- "browser.search.serpEventTelemetry.enabled",
- true
-);
-
const CATEGORIZATION_PREF =
"browser.search.serpEventTelemetryCategorization.enabled";
@@ -83,15 +80,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 +382,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) {
@@ -491,11 +493,7 @@ class TelemetryHandler {
// from something that happened in content. We keep this separate from
// source because legacy telemetry should not change its reporting.
let inContentSource;
- if (
- lazy.serpEventsEnabled &&
- info.hasComponents &&
- this.#browserContentSourceMap.has(browser)
- ) {
+ if (info.hasComponents && this.#browserContentSourceMap.has(browser)) {
inContentSource = this.#browserContentSourceMap.get(browser);
this.#browserContentSourceMap.delete(browser);
}
@@ -508,7 +506,7 @@ class TelemetryHandler {
}
let impressionId;
- if (lazy.serpEventsEnabled && info.hasComponents) {
+ if (info.hasComponents) {
// The UUID generated by Services.uuid contains leading and trailing braces.
// Need to trim them first.
impressionId = Services.uuid.generateUUID().toString().slice(1, -1);
@@ -526,7 +524,7 @@ class TelemetryHandler {
let item = this._browserInfoByURL.get(urlKey);
let impressionInfo;
- if (lazy.serpEventsEnabled && info.hasComponents) {
+ if (info.hasComponents) {
let partnerCode = "";
if (info.code != "none" && info.code != null) {
partnerCode = info.code;
@@ -538,6 +536,7 @@ class TelemetryHandler {
source: inContentSource ?? source,
isShoppingPage: info.isShoppingPage,
isPrivate: lazy.PrivateBrowsingUtils.isBrowserPrivate(browser),
+ isSignedIn: info.isSignedIn,
};
}
@@ -551,6 +550,8 @@ class TelemetryHandler {
searchBoxSubmitted: false,
categorizationInfo: null,
adsClicked: 0,
+ adsHidden: 0,
+ adsLoaded: 0,
adsVisible: 0,
searchQuery: info.searchQuery,
});
@@ -568,6 +569,8 @@ class TelemetryHandler {
searchBoxSubmitted: false,
categorizationInfo: null,
adsClicked: 0,
+ adsHidden: 0,
+ adsLoaded: 0,
adsVisible: 0,
searchQuery: info.searchQuery,
}),
@@ -1047,19 +1050,27 @@ class TelemetryHandler {
}
let isShoppingPage = false;
let hasComponents = false;
- if (lazy.serpEventsEnabled) {
- if (searchProviderInfo.shoppingTab?.regexp) {
- isShoppingPage = searchProviderInfo.shoppingTab.regexp.test(url);
- }
- if (searchProviderInfo.components?.length) {
- hasComponents = true;
- }
+ let isSignedIn = false;
+ if (searchProviderInfo.shoppingTab?.regexp) {
+ isShoppingPage = searchProviderInfo.shoppingTab.regexp.test(url);
+ }
+ if (searchProviderInfo.components?.length) {
+ hasComponents = true;
+ }
+ if (searchProviderInfo.accountCookies) {
+ isSignedIn = searchProviderInfo.accountCookies.some(cookieObj => {
+ return Services.cookies
+ .getCookiesFromHost(cookieObj.host, {})
+ .some(c => c.name == cookieObj.name);
+ });
}
+
return {
provider: searchProviderInfo.telemetryId,
type,
code,
isShoppingPage,
+ isSignedIn,
hasComponents,
searchQuery,
isSPA,
@@ -1332,10 +1343,6 @@ class ContentHandler {
* The search provider info associated with the item.
*/
#maybeRecordSERPTelemetry(wrappedChannel, item, info) {
- if (!lazy.serpEventsEnabled) {
- return;
- }
-
if (wrappedChannel._recordedClick) {
lazy.logConsole.debug("Click already recorded.");
return;
@@ -1635,13 +1642,17 @@ class ContentHandler {
}
let telemetryState = item.browserTelemetryStateMap.get(browser);
if (
- lazy.serpEventsEnabled &&
info.adImpressions &&
telemetryState &&
!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.adsHidden += data.adsHidden;
+ telemetryState.adsLoaded += data.adsLoaded;
+ telemetryState.adsVisible += data.adsVisible;
+ }
lazy.logConsole.debug("Counting ad:", { type: componentType, ...data });
Glean.serp.adImpression.record({
@@ -1743,6 +1754,7 @@ class ContentHandler {
shopping_tab_displayed: info.shoppingTabDisplayed,
is_shopping_page: impressionInfo.isShoppingPage,
is_private: impressionInfo.isPrivate,
+ is_signed_in: impressionInfo.isSignedIn,
});
lazy.logConsole.debug(`Reported Impression:`, {
impressionId,
@@ -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,7 +1803,10 @@ class ContentHandler {
partner_code: impressionInfo.partnerCode,
provider: impressionInfo.provider,
tagged: impressionInfo.tagged,
+ is_shopping_page: impressionInfo.isShoppingPage,
num_ads_clicked: telemetryState.adsClicked,
+ num_ads_hidden: telemetryState.adsHidden,
+ num_ads_loaded: telemetryState.adsLoaded,
num_ads_visible: telemetryState.adsVisible,
});
};
@@ -1829,6 +1846,10 @@ class ContentHandler {
* @typedef {object} CategorizationExtraParams
* @property {number} num_ads_clicked
* The total number of ads clicked on a SERP.
+ * @property {number} num_ads_hidden
+ * The total number of ads hidden from the user when categorization occured.
+ * @property {number} num_ads_loaded
+ * The total number of ads loaded when categorization occured.
* @property {number} num_ads_visible
* The total number of ads visible to the user when categorization occured.
*/
@@ -1843,6 +1864,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 +2036,8 @@ class CategorizationEventScheduler {
*/
#mostRecentMs = null;
- constructor() {
- this.init();
- }
-
init() {
- if (!lazy.serpEventTelemetryCategorization || this.#init) {
+ if (this.#init) {
return;
}
@@ -2114,6 +2147,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 +2213,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 +2262,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 +2311,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 +2380,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 +2414,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 +2432,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 +2461,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 +2495,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 +2541,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 +2605,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);
- }
-
- this.#empty = !hasResults;
+ await this.#store.insertFileContents(fileContents, this.#version);
- 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 +2636,8 @@ class DomainToCategoriesMap {
#createTimerToPopulateMap() {
if (
this.#downloadRetries >=
- TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxTriesPerSession
+ TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxTriesPerSession ||
+ !this.#client
) {
return;
}
@@ -2486,7 +2657,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 +2670,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/content/autocomplete-popup.js b/browser/components/search/content/autocomplete-popup.js
index c5a33348ff..8736c683a7 100644
--- a/browser/components/search/content/autocomplete-popup.js
+++ b/browser/components/search/content/autocomplete-popup.js
@@ -7,6 +7,7 @@
// Wrap in a block to prevent leaking to window scope.
{
ChromeUtils.defineESModuleGetters(this, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
SearchOneOffs: "resource:///modules/SearchOneOffs.sys.mjs",
});
@@ -194,7 +195,7 @@
let search = this.input.controller.getValueAt(this.selectedIndex);
// open the search results according to the clicking subtlety
- let where = whereToOpenLink(aEvent, false, true);
+ let where = BrowserUtils.whereToOpenLink(aEvent, false, true);
let params = {};
// But open ctrl/cmd clicks on autocomplete items in a new background tab.
diff --git a/browser/components/search/content/searchbar.js b/browser/components/search/content/searchbar.js
index c872236472..e0fe7f41e3 100644
--- a/browser/components/search/content/searchbar.js
+++ b/browser/components/search/content/searchbar.js
@@ -12,6 +12,7 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
SearchSuggestionController:
"resource://gre/modules/SearchSuggestionController.sys.mjs",
@@ -317,7 +318,7 @@
if (aEvent.button == 2) {
return;
}
- where = whereToOpenLink(aEvent, false, true);
+ where = lazy.BrowserUtils.whereToOpenLink(aEvent, false, true);
if (
newTabPref &&
!aEvent.altKey &&
@@ -885,12 +886,13 @@
goDoCommand("cmd_paste");
this.handleSearchCommand(event);
break;
- case clearHistoryItem:
+ case clearHistoryItem: {
let param = this.textbox.getAttribute("autocompletesearchparam");
lazy.FormHistory.update({ op: "remove", fieldname: param });
this.textbox.value = "";
break;
- default:
+ }
+ default: {
let cmd = event.originalTarget.getAttribute("cmd");
if (cmd) {
let controller =
@@ -898,6 +900,7 @@
controller.doCommand(cmd);
}
break;
+ }
}
});
}
diff --git a/browser/components/search/metrics.yaml b/browser/components/search/metrics.yaml
index 12fd44a0e2..8f81eff34b 100644
--- a/browser/components/search/metrics.yaml
+++ b/browser/components/search/metrics.yaml
@@ -193,6 +193,11 @@ serp:
description:
Indicates if the page was loaded while in Private Browsing Mode.
type: boolean
+ is_signed_in:
+ description:
+ Indicates if the page was loaded while the user is signed in to a
+ provider's account.
+ type: boolean
engagement:
type: event
@@ -331,6 +336,121 @@ 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
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1892267
+ 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_hidden:
+ description: >
+ Number of ads hidden on the page at the time of categorizing the
+ page.
+ type: quantity
+ num_ads_loaded:
+ description: >
+ Number of ads loaded on the page at the time of categorizing the
+ page.
+ type: quantity
+ 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..5a6f87f336 100644
--- a/browser/components/search/schema/search-telemetry-ui-schema.json
+++ b/browser/components/search/schema/search-telemetry-v2-ui-schema.json
@@ -14,6 +14,8 @@
"extraAdServersRegexps",
"adServerAttributes",
"components",
+ "ignoreLinkRegexps",
+ "nonAdsLinkQueryParamNames",
"nonAdsLinkRegexps",
"shoppingTab",
"domainExtraction",
diff --git a/browser/components/search/test/browser/telemetry/browser.toml b/browser/components/search/test/browser/telemetry/browser.toml
index 660fc4eae2..c8cb201324 100644
--- a/browser/components/search/test/browser/telemetry/browser.toml
+++ b/browser/components/search/test/browser/telemetry/browser.toml
@@ -6,9 +6,6 @@ prefs = ["browser.search.log=true"]
["browser_search_glean_serp_event_telemetry_categorization_enabled_by_nimbus_variable.js"]
support-files = ["searchTelemetryDomainCategorizationReporting.html"]
-["browser_search_glean_serp_event_telemetry_enabled_by_nimbus_variable.js"]
-support-files = ["searchTelemetryAd.html"]
-
["browser_search_telemetry_abandonment.js"]
support-files = [
"searchTelemetry.html",
@@ -50,6 +47,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 +109,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 +117,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",
@@ -166,6 +172,9 @@ support-files = [
["browser_search_telemetry_shopping.js"]
support-files = ["searchTelemetryAd_shopping.html"]
+["browser_search_telemetry_signed_in_to_account.js"]
+support-files = ["searchTelemetry.html"]
+
["browser_search_telemetry_sources.js"]
support-files = ["searchTelemetry.html", "searchTelemetryAd.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..d6ff3e2ed9 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,7 +160,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
},
]);
@@ -160,6 +171,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 +191,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_glean_serp_event_telemetry_enabled_by_nimbus_variable.js b/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_enabled_by_nimbus_variable.js
deleted file mode 100644
index 096178499b..0000000000
--- a/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_enabled_by_nimbus_variable.js
+++ /dev/null
@@ -1,167 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// Test to verify we can toggle the Glean SERP event telemetry feature via a
-// Nimbus variable.
-
-const lazy = {};
-
-const TELEMETRY_PREF = "browser.search.serpEventTelemetry.enabled";
-
-ChromeUtils.defineESModuleGetters(lazy, {
- ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
- ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
-});
-
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- "serpEventsEnabled",
- TELEMETRY_PREF,
- false
-);
-
-const TEST_PROVIDER_INFO = [
- {
- telemetryId: "example",
- searchPageRegexp:
- /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetry(?:Ad)?.html/,
- queryParamNames: ["s"],
- codeParamName: "abc",
- taggedCodes: ["ff"],
- followOnParamNames: ["a"],
- extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
- components: [
- {
- type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
- default: true,
- },
- ],
- },
-];
-
-async function verifyEventsRecorded() {
- resetTelemetry();
-
- let tab = await BrowserTestUtils.openNewForegroundTab(
- gBrowser,
- getSERPUrl("searchTelemetryAd.html")
- );
- await waitForPageWithAdImpressions();
-
- assertSERPTelemetry([
- {
- impression: {
- provider: "example",
- tagged: "true",
- partner_code: "ff",
- source: "unknown",
- is_shopping_page: "false",
- is_private: "false",
- shopping_tab_displayed: "false",
- },
- adImpressions: [
- {
- component: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
- ads_loaded: "2",
- ads_visible: "2",
- ads_hidden: "0",
- },
- ],
- },
- ]);
-
- BrowserTestUtils.removeTab(tab);
-
- assertSERPTelemetry([
- {
- impression: {
- provider: "example",
- tagged: "true",
- partner_code: "ff",
- source: "unknown",
- is_shopping_page: "false",
- is_private: "false",
- shopping_tab_displayed: "false",
- },
- adImpressions: [
- {
- component: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
- ads_loaded: "2",
- ads_visible: "2",
- ads_hidden: "0",
- },
- ],
- abandonment: {
- reason: SearchSERPTelemetryUtils.ABANDONMENTS.TAB_CLOSE,
- },
- },
- ]);
-}
-
-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;
-
- registerCleanupFunction(async () => {
- SearchSERPTelemetry.overrideSearchTelemetryForTests();
- Services.telemetry.canRecordExtended = oldCanRecord;
- await SpecialPowers.popPrefEnv();
- resetTelemetry();
- });
-});
-
-add_task(async function test_enable_experiment_when_pref_is_not_enabled() {
- let prefBranch = Services.prefs.getDefaultBranch("");
- let originalPrefValue = prefBranch.getBoolPref(TELEMETRY_PREF);
-
- // Ensure the build being tested has the preference value as false.
- // Changing the preference in the test must be done on the default branch
- // because in the telemetry code, we're referencing the preference directly
- // instead of through NimbusFeatures. Enrolling in an experiment will change
- // the default branch, and not overwrite the user branch.
- prefBranch.setBoolPref(TELEMETRY_PREF, false);
-
- Assert.equal(
- lazy.serpEventsEnabled,
- false,
- "serpEventsEnabled should be false when not enrolled in experiment."
- );
-
- await lazy.ExperimentAPI.ready();
-
- let doExperimentCleanup = await lazy.ExperimentFakes.enrollWithFeatureConfig(
- {
- featureId: NimbusFeatures.search.featureId,
- value: {
- serpEventTelemetryEnabled: true,
- },
- },
- { isRollout: true }
- );
-
- Assert.equal(
- lazy.serpEventsEnabled,
- true,
- "serpEventsEnabled should be true when enrolled in experiment."
- );
-
- // To ensure Nimbus set "browser.search.serpEventTelemetry.enabled" to true,
- // we test that an impression, ad_impression and abandonment event are
- // recorded correctly.
- await verifyEventsRecorded();
-
- await doExperimentCleanup();
-
- Assert.equal(
- lazy.serpEventsEnabled,
- false,
- "serpEventsEnabled should be false after experiment."
- );
-
- // Clean up.
- prefBranch.setBoolPref(TELEMETRY_PREF, originalPrefValue);
-});
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_abandonment.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_abandonment.js
index 0c1d8b8234..799c5018e9 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_abandonment.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_abandonment.js
@@ -2,7 +2,7 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*
- * Tests for the Glean SERP abandonment event
+ * Tests for the Glean SERP abandonment event.
*/
"use strict";
@@ -32,9 +32,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -65,6 +62,7 @@ add_task(async function test_tab_close() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.TAB_CLOSE,
@@ -99,6 +97,7 @@ add_task(async function test_window_close() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.WINDOW_CLOSE,
@@ -134,6 +133,7 @@ add_task(async function test_navigation_via_urlbar() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION,
@@ -181,6 +181,7 @@ add_task(async function test_navigation_via_back_button() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION,
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component.js
index 5a09353ed6..dd7629f5f3 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component.js
@@ -103,9 +103,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
// The tests evaluate whether or not ads are visible depending on whether
// they are within the view of the window. To ensure the test results
@@ -142,6 +139,7 @@ add_task(async function test_ad_impressions_with_one_carousel() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -186,6 +184,7 @@ add_task(async function test_ad_impressions_with_two_carousels() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -221,6 +220,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -272,6 +272,7 @@ add_task(async function test_ad_impressions_with_carousels_tabhistory() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -304,6 +305,7 @@ add_task(async function test_ad_impressions_with_hidden_carousels() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -338,6 +340,7 @@ add_task(async function test_ad_impressions_with_carousel_scrolled_left() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -372,6 +375,7 @@ add_task(async function test_ad_impressions_with_carousel_below_the_fold() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -404,6 +408,7 @@ add_task(async function test_ad_impressions_with_text_links() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -463,6 +468,7 @@ add_task(async function test_ad_visibility() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -495,6 +501,7 @@ add_task(async function test_impressions_without_ads() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -527,6 +534,7 @@ add_task(async function test_ad_impressions_with_cookie_banner() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_children.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_children.js
index 65cd612a49..6f4f5d7ec1 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_children.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_children.js
@@ -28,6 +28,7 @@ const IMPRESSION = {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
};
const SERP_URL = getSERPUrl("searchTelemetryAd_searchbox_with_content.html");
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_parent.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_parent.js
index 8471215840..e6e5f04cf9 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_parent.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_adImpression_component_skipCount_parent.js
@@ -28,6 +28,7 @@ const IMPRESSION = {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
};
const SERP_URL = getSERPUrl("searchTelemetryAd_searchbox_with_content.html");
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_timing.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_timing.js
index 9ecc4e8d92..a43fae7ca2 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_timing.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_timing.js
@@ -30,9 +30,6 @@ const TEST_PROVIDER_INFO = [
add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
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..a9201beb22 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,7 +113,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
},
]);
@@ -143,7 +156,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
},
]);
@@ -181,7 +197,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
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..694900912d 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,9 @@ add_task(async function test_download_after_failure() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
+ num_ads_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
num_ads_clicked: "0",
},
@@ -166,6 +188,7 @@ add_task(async function test_download_after_failure() {
// Clean up.
await SpecialPowers.popPrefEnv();
+ await waitForDomainToCategoriesUninit();
await resetCategorizationCollection(record);
});
@@ -214,6 +237,7 @@ add_task(async function test_download_after_multiple_failures() {
// Clean up.
await SpecialPowers.popPrefEnv();
+ await waitForDomainToCategoriesUninit();
await resetCategorizationCollection(record);
});
@@ -245,6 +269,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 +288,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 +334,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..f23859dbd5
--- /dev/null
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_no_sponsored_values.js
@@ -0,0 +1,144 @@
+/* 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: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ is_private: "false",
+ shopping_tab_displayed: "true",
+ is_signed_in: "false",
+ },
+ 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_hidden: "0",
+ num_ads_loaded: "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..45d97c85e8 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,7 +124,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
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..fba30ec4a0 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,7 +134,10 @@ add_task(async function test_categorization_reporting() {
partner_code: "ff",
provider: "example",
tagged: "true",
+ is_shopping_page: "false",
num_ads_clicked: "0",
+ num_ads_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
},
]);
@@ -147,6 +169,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 +200,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 +242,50 @@ 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_hidden: "0",
+ num_ads_loaded: "12",
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_hidden: "0",
+ num_ads_loaded: "2",
+ 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..59a3c15ef9 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,7 +137,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
},
]);
@@ -170,7 +184,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
});
}
@@ -223,7 +240,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
},
]);
@@ -276,7 +296,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
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..b824ec9817 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,7 +149,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
},
]);
@@ -195,7 +209,10 @@ 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_hidden: "0",
+ num_ads_loaded: "2",
num_ads_visible: "2",
},
]);
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached.js
index 791e29a01f..7392c15396 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached.js
@@ -89,9 +89,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -142,6 +139,7 @@ add_task(async function test_click_cached_page() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -173,6 +171,7 @@ add_task(async function test_click_cached_page() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached_serp.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached_serp.js
index 72e26639fb..13861f4b27 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached_serp.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_cached_serp.js
@@ -48,9 +48,6 @@ const TEST_PROVIDER_INFO = [
add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_content.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_content.js
index f94e6b0bd8..993568abe6 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_content.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_content.js
@@ -75,9 +75,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -113,6 +110,7 @@ add_task(async function test_click_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
@@ -144,6 +142,7 @@ add_task(async function test_click_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -192,6 +191,7 @@ add_task(async function test_click_shopping() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
@@ -223,6 +223,7 @@ add_task(async function test_click_shopping() {
is_shopping_page: "true",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -275,6 +276,7 @@ add_task(async function test_click_related_search_in_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
@@ -306,6 +308,7 @@ add_task(async function test_click_related_search_in_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -361,6 +364,7 @@ add_task(async function test_click_redirect_search_in_newtab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
@@ -392,6 +396,7 @@ add_task(async function test_click_redirect_search_in_newtab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -454,6 +459,7 @@ add_task(async function test_content_source_reset() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
@@ -489,6 +495,7 @@ add_task(async function test_content_source_reset() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
@@ -520,6 +527,7 @@ add_task(async function test_content_source_reset() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -580,6 +588,7 @@ add_task(async function test_click_refinement_button() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
@@ -611,6 +620,7 @@ add_task(async function test_click_refinement_button() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
adImpressions: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_children.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_children.js
index 4f5aaf9378..dce34e9443 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_children.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_children.js
@@ -28,6 +28,7 @@ const IMPRESSION = {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
};
const SELECTOR = ".arrow";
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_parent.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_parent.js
index 4e3c635b4c..6d509ba904 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_parent.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_eventListeners_parent.js
@@ -29,6 +29,7 @@ const IMPRESSION = {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
};
const SELECTOR = ".arrow";
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_ignoreLinkRegexps.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_ignoreLinkRegexps.js
index 10f2a2d836..86e9779d0b 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_ignoreLinkRegexps.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_ignoreLinkRegexps.js
@@ -49,6 +49,7 @@ const IMPRESSION = {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
};
const SERP_URL = getSERPUrl("searchTelemetryAd_searchbox_with_content.html");
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_multiple_tabs.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_multiple_tabs.js
index fbe6f4fc73..0e76c0c8a1 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_multiple_tabs.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_multiple_tabs.js
@@ -63,10 +63,7 @@ add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
await SpecialPowers.pushPrefEnv({
- set: [
- ["browser.search.serpEventTelemetry.enabled", true],
- ["dom.ipc.processCount.webIsolated", MAX_IPC],
- ],
+ set: [["dom.ipc.processCount.webIsolated", MAX_IPC]],
});
registerCleanupFunction(async () => {
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_nonAdsLinkQueryParamNames.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_nonAdsLinkQueryParamNames.js
index 93a6b7993e..b334aa5637 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_nonAdsLinkQueryParamNames.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_nonAdsLinkQueryParamNames.js
@@ -76,6 +76,7 @@ add_task(async function test_click_absolute_url_in_query_param() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
@@ -101,6 +102,7 @@ add_task(async function test_click_absolute_url_in_query_param() {
is_shopping_page: "true",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -151,6 +153,7 @@ add_task(async function test_click_relative_href_in_query_param() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
@@ -176,6 +179,7 @@ add_task(async function test_click_relative_href_in_query_param() {
is_shopping_page: "true",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -226,6 +230,7 @@ add_task(async function test_click_irrelevant_href_in_query_param() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_non_ad.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_non_ad.js
index d351234d50..94e8ab24fe 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_non_ad.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_non_ad.js
@@ -31,10 +31,6 @@ const TEST_PROVIDER_INFO = [
add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
- // Enable local telemetry recording for the duration of the tests.
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -71,6 +67,7 @@ add_task(async function test_click_non_ads_link() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -128,6 +125,7 @@ add_task(async function test_click_non_ad_with_no_ads() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_query_params.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_query_params.js
index 6d93707d68..da8c952191 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_query_params.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_query_params.js
@@ -5,7 +5,6 @@
* These tests load SERPs and check that query params that are changed either
* by the browser or in the page after click are still properly recognized
* as ads.
- *
*/
"use strict";
@@ -50,9 +49,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -88,6 +84,7 @@ add_task(async function test_click_links() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -136,6 +133,7 @@ add_task(async function test_click_links() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -167,6 +165,7 @@ add_task(async function test_click_links() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -231,6 +230,7 @@ add_task(async function test_click_link_with_more_parameters() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -294,6 +294,7 @@ add_task(async function test_click_link_with_fewer_parameters() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -357,6 +358,7 @@ add_task(async function test_click_link_with_reordered_parameters() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_redirect.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_redirect.js
index 5d7f2ee408..7851cbb7e7 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_redirect.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_redirect.js
@@ -36,10 +36,6 @@ const TEST_PROVIDER_INFO = [
add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
- // Enable local telemetry recording for the duration of the tests.
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -79,6 +75,7 @@ add_task(async function test_click_non_ads_link_redirected() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -139,6 +136,7 @@ add_task(async function test_click_non_ads_link_redirected_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -195,6 +193,7 @@ add_task(async function test_click_non_ads_link_redirect_non_top_level() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -248,6 +247,7 @@ add_task(async function test_multiple_redirects_non_ad_link() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -301,6 +301,7 @@ add_task(async function test_click_ad_link_redirected() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -349,6 +350,7 @@ add_task(async function test_click_ad_link_redirected_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_target.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_target.js
index 8f7f7f4e05..5af024222c 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_target.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_engagement_target.js
@@ -148,9 +148,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -189,6 +186,7 @@ add_task(async function test_click_second_ad_in_component() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -245,6 +243,7 @@ add_task(async function test_click_ads_link_modified() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -300,6 +299,7 @@ add_task(async function test_click_and_submit_incontent_searchbox() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -321,6 +321,7 @@ add_task(async function test_click_and_submit_incontent_searchbox() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
},
]);
@@ -358,6 +359,7 @@ add_task(async function test_click_autosuggest() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -375,6 +377,7 @@ add_task(async function test_click_autosuggest() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
},
]);
@@ -404,6 +407,7 @@ add_task(async function test_click_carousel_expand() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -464,6 +468,7 @@ add_task(async function test_click_link_with_special_characters_in_path() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -519,6 +524,7 @@ add_task(async function test_click_cookie_banner_accept() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -564,6 +570,7 @@ add_task(async function test_click_cookie_banner_reject() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -609,6 +616,7 @@ add_task(async function test_click_cookie_banner_more_options() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_new_window.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_new_window.js
index 4f943fe92d..8866f72579 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_new_window.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_new_window.js
@@ -30,10 +30,6 @@ const TEST_PROVIDER_INFO = [
add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
- // Enable local telemetry recording for the duration of the tests.
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -83,6 +79,7 @@ add_task(async function load_serp_in_new_window_with_pref_and_click_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -148,6 +145,7 @@ add_task(async function load_serp_in_new_window_with_pref_and_click_organic() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -214,6 +212,7 @@ add_task(async function load_serp_in_new_window_with_context_menu() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -303,6 +302,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -322,6 +322,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_private.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_private.js
index ea7556c8f6..2c4494e202 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_private.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_private.js
@@ -31,10 +31,6 @@ const TEST_PROVIDER_INFO = [
add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
- // Enable local telemetry recording for the duration of the tests.
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -82,6 +78,7 @@ add_task(async function load_2_pbm_serps_and_1_non_pbm_serp() {
is_shopping_page: "false",
is_private: "true",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.NAVIGATION,
@@ -96,6 +93,7 @@ add_task(async function load_2_pbm_serps_and_1_non_pbm_serp() {
is_shopping_page: "false",
is_private: "true",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.TAB_CLOSE,
@@ -118,6 +116,7 @@ add_task(async function load_2_pbm_serps_and_1_non_pbm_serp() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_remote_settings_sync.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_remote_settings_sync.js
index 5f2afcf6fc..a8a52ba794 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_remote_settings_sync.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_remote_settings_sync.js
@@ -86,7 +86,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
await SpecialPowers.pushPrefEnv({
set: [
- ["browser.search.serpEventTelemetry.enabled", true],
// Set the IPC count to a small number so that we only have to open
// one additional tab to reuse the same process.
["dom.ipc.processCount.webIsolated", 1],
@@ -139,6 +138,7 @@ add_task(async function update_telemetry_tab_already_open() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -192,6 +192,7 @@ add_task(async function update_telemetry_tab_closed() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -246,6 +247,7 @@ add_task(async function update_telemetry_multiple_tabs() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -307,6 +309,7 @@ add_task(async function update_telemetry_multiple_processes_and_tabs() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_shopping.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_shopping.js
index e2352b53f4..b687f025fd 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_shopping.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_shopping.js
@@ -55,9 +55,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -83,6 +80,7 @@ async function loadSerpAndClickShoppingTab(page) {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -109,6 +107,7 @@ async function loadSerpAndClickShoppingTab(page) {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "true",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_signed_in_to_account.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_signed_in_to_account.js
new file mode 100644
index 0000000000..701016e5b0
--- /dev/null
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_signed_in_to_account.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test to verify that the SERP impression event correctly records whether the
+ * user is logged in to a provider's account at the time of SERP load.
+ */
+
+"use strict";
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp:
+ /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/telemetry\/searchTelemetry(?:Ad)?/,
+ queryParamNames: ["s"],
+ accountCookies: [
+ {
+ host: "accounts.google.com",
+ name: "SID",
+ },
+ ],
+ codeParamName: "abc",
+ taggedCodes: ["ff"],
+ followOnParamNames: ["a"],
+ extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
+ components: [
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
+ default: true,
+ },
+ ],
+ },
+];
+
+function simulateGoogleAccountSignIn() {
+ // Manually set the cookie that is present when the client is signed in to a
+ // Google account.
+ Services.cookies.add(
+ "accounts.google.com",
+ "",
+ "SID",
+ "dummy_cookie_value",
+ false,
+ false,
+ false,
+ Date.now() + 1000 * 60 * 60,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+}
+
+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;
+
+ registerCleanupFunction(async () => {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ resetTelemetry();
+ });
+});
+
+add_task(async function test_not_signed_in_to_google_account() {
+ info("Loading SERP while not signed in to Google account.");
+ let url = getSERPUrl("searchTelemetry.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ assertSERPTelemetry([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ is_private: "false",
+ shopping_tab_displayed: "false",
+ is_signed_in: "false",
+ },
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab);
+ resetTelemetry();
+});
+
+add_task(async function test_signed_in_to_google_account() {
+ simulateGoogleAccountSignIn();
+
+ info("Loading SERP while signed in to Google account.");
+ let url = getSERPUrl("searchTelemetry.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ assertSERPTelemetry([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ is_private: "false",
+ shopping_tab_displayed: "false",
+ is_signed_in: "true",
+ },
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+ resetTelemetry();
+});
+
+add_task(async function test_toggle_google_account_signed_in_status() {
+ info("Loading SERP while not signed in to Google account.");
+ let url = getSERPUrl("searchTelemetry.html");
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ assertSERPTelemetry([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ is_private: "false",
+ shopping_tab_displayed: "false",
+ is_signed_in: "false",
+ },
+ },
+ ]);
+
+ resetTelemetry();
+
+ info("Signing in to Google account.");
+ simulateGoogleAccountSignIn();
+
+ info("Loading SERP after signing in to Google account.");
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ assertSERPTelemetry([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ is_private: "false",
+ shopping_tab_displayed: "false",
+ is_signed_in: "true",
+ },
+ },
+ ]);
+
+ resetTelemetry();
+
+ info("Signing out of Google account.");
+ Services.cookies.removeAll();
+
+ info("Loading SERP after signing out of Google account.");
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ assertSERPTelemetry([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ is_private: "false",
+ shopping_tab_displayed: "false",
+ is_signed_in: "false",
+ },
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ resetTelemetry();
+});
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources.js
index 7fa66a1adf..3cb246f21d 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources.js
@@ -63,7 +63,6 @@ add_setup(async function () {
],
// Ensure to add search suggestion telemetry as search_suggestion not search_formhistory.
["browser.urlbar.maxHistoricalSearchSuggestions", 0],
- ["browser.search.serpEventTelemetry.enabled", true],
],
});
// Enable local telemetry recording for the duration of the tests.
@@ -148,6 +147,7 @@ async function track_ad_click(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_about.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_about.js
index a313c75ac7..fa596acac1 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_about.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_about.js
@@ -4,7 +4,6 @@
/*
* Main tests for SearchSERPTelemetry - general engine visiting and link
* clicking on about pages.
- *
*/
"use strict";
@@ -61,7 +60,6 @@ add_setup(async function () {
],
// Ensure to add search suggestion telemetry as search_suggestion not search_formhistory.
["browser.urlbar.maxHistoricalSearchSuggestions", 0],
- ["browser.search.serpEventTelemetry.enabled", true],
],
});
// Enable local telemetry recording for the duration of the tests.
@@ -146,6 +144,7 @@ async function track_ad_click(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads.js
index 0fd93da30f..97480b7d36 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads.js
@@ -38,9 +38,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -77,6 +74,7 @@ add_task(async function test_simple_search_page_visit() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.TAB_CLOSE,
@@ -120,6 +118,7 @@ add_task(async function test_simple_search_page_visit_telemetry() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.TAB_CLOSE,
@@ -156,6 +155,7 @@ add_task(async function test_follow_on_visit() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.TAB_CLOSE,
@@ -170,6 +170,7 @@ add_task(async function test_follow_on_visit() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
abandonment: {
reason: SearchSERPTelemetryUtils.ABANDONMENTS.TAB_CLOSE,
@@ -205,6 +206,7 @@ add_task(async function test_track_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -247,6 +249,7 @@ add_task(async function test_track_ad_organic() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -294,6 +297,7 @@ add_task(async function test_track_ad_new_window() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -349,6 +353,7 @@ add_task(async function test_track_ad_pages_without_ads() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
},
{
@@ -360,6 +365,7 @@ add_task(async function test_track_ad_pages_without_ads() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_clicks.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_clicks.js
index 11d2176563..1b7dee7f04 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_clicks.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_clicks.js
@@ -34,9 +34,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -82,6 +79,7 @@ async function track_ad_click(testOrganic) {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -118,6 +116,7 @@ async function track_ad_click(testOrganic) {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -164,6 +163,7 @@ async function track_ad_click(testOrganic) {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -189,6 +189,7 @@ async function track_ad_click(testOrganic) {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -228,6 +229,7 @@ async function track_ad_click(testOrganic) {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -253,6 +255,7 @@ async function track_ad_click(testOrganic) {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -307,6 +310,7 @@ add_task(async function test_track_ad_click_with_location_change_other_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -350,6 +354,7 @@ add_task(async function test_track_ad_click_with_location_change_other_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_data_attributes.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_data_attributes.js
index 3c5e0a464e..02b716c423 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_data_attributes.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_data_attributes.js
@@ -2,7 +2,8 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*
- * Tests for SearchSERPTelemetry associated with ad links found in data attributes.
+ * Tests for SearchSERPTelemetry associated with ad links found in data
+ * attributes.
*/
"use strict";
@@ -34,9 +35,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -76,6 +74,7 @@ add_task(async function test_track_ad_on_data_attributes() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -122,6 +121,7 @@ add_task(async function test_track_ad_on_data_attributes_and_hrefs() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -165,6 +165,7 @@ add_task(async function test_track_no_ad_on_data_attributes_and_hrefs() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
},
]);
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_load_events.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_load_events.js
index 069e13d339..b79f31b678 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_load_events.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_ads_load_events.js
@@ -34,9 +34,6 @@ add_setup(async function () {
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
registerCleanupFunction(async () => {
SearchSERPTelemetry.overrideSearchTelemetryForTests();
@@ -84,6 +81,7 @@ add_task(async function test_track_ad_on_DOMContentLoaded() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -126,6 +124,7 @@ add_task(async function test_track_ad_on_load_event() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_in_content.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_in_content.js
index 9bff667857..a7237506f6 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_in_content.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_in_content.js
@@ -43,9 +43,6 @@ const TEST_PROVIDER_INFO = [
add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
- await SpecialPowers.pushPrefEnv({
- set: [["browser.search.serpEventTelemetry.enabled", true]],
- });
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
Services.telemetry.canRecordExtended = true;
@@ -88,6 +85,7 @@ add_task(async function test_source_opened_in_new_tab_via_middle_click() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -113,6 +111,7 @@ add_task(async function test_source_opened_in_new_tab_via_middle_click() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -162,6 +161,7 @@ add_task(async function test_source_opened_in_new_tab_via_target_blank() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -187,6 +187,7 @@ add_task(async function test_source_opened_in_new_tab_via_target_blank() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -250,6 +251,7 @@ add_task(async function test_source_opened_in_new_tab_via_context_menu() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -275,6 +277,7 @@ add_task(async function test_source_opened_in_new_tab_via_context_menu() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -317,6 +320,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -343,6 +347,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -386,6 +391,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -412,6 +418,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -464,6 +471,7 @@ add_task(async function test_refinement_button_vs_opened_in_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -489,6 +497,7 @@ add_task(async function test_refinement_button_vs_opened_in_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_navigation.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_navigation.js
index 7ce681701a..58519d82ac 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_navigation.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_navigation.js
@@ -56,10 +56,7 @@ add_setup(async function () {
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
await waitForIdle();
await SpecialPowers.pushPrefEnv({
- set: [
- ["browser.urlbar.suggest.searches", true],
- ["browser.search.serpEventTelemetry.enabled", true],
- ],
+ set: [["browser.urlbar.suggest.searches", true]],
});
// Enable local telemetry recording for the duration of the tests.
let oldCanRecord = Services.telemetry.canRecordExtended;
@@ -130,6 +127,7 @@ add_task(async function test_search() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -172,6 +170,7 @@ add_task(async function test_reload() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -194,6 +193,7 @@ add_task(async function test_reload() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -233,6 +233,7 @@ add_task(async function test_reload() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -255,6 +256,7 @@ add_task(async function test_reload() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -306,6 +308,7 @@ add_task(async function test_fresh_search() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -345,6 +348,7 @@ add_task(async function test_click_ad() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -394,6 +398,7 @@ add_task(async function test_go_back() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -419,6 +424,7 @@ add_task(async function test_go_back() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -459,6 +465,7 @@ add_task(async function test_go_back() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -484,6 +491,7 @@ add_task(async function test_go_back() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -538,6 +546,7 @@ add_task(async function test_fresh_search_with_urlbar_persisted() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -577,6 +586,7 @@ add_task(async function test_fresh_search_with_urlbar_persisted() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -599,6 +609,7 @@ add_task(async function test_fresh_search_with_urlbar_persisted() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -640,6 +651,7 @@ add_task(async function test_fresh_search_with_urlbar_persisted() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -662,6 +674,7 @@ add_task(async function test_fresh_search_with_urlbar_persisted() {
shopping_tab_displayed: "false",
is_shopping_page: "false",
is_private: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_webextension.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_webextension.js
index cb9e123622..1d31fc4456 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_webextension.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources_webextension.js
@@ -4,7 +4,6 @@
/*
* Main tests for SearchSERPTelemetry - general engine visiting and
* link clicking with Web Extensions.
- *
*/
"use strict";
@@ -42,7 +41,6 @@ add_setup(async function () {
],
// Ensure to add search suggestion telemetry as search_suggestion not search_formhistory.
["browser.urlbar.maxHistoricalSearchSuggestions", 0],
- ["browser.search.serpEventTelemetry.enabled", true],
],
});
// Enable local telemetry recording for the duration of the tests.
@@ -127,6 +125,7 @@ async function track_ad_click(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_in_content.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_in_content.js
index 39270c7e9f..f0d731c577 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_in_content.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_in_content.js
@@ -52,6 +52,7 @@ add_task(async function test_content_process_type_search_click_suggestion() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -81,6 +82,7 @@ add_task(async function test_content_process_type_search_click_suggestion() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -125,6 +127,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -154,6 +157,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -197,6 +201,7 @@ add_task(async function test_content_process_engagement() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -246,6 +251,7 @@ add_task(async function test_content_process_engagement_that_changes_page() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -271,6 +277,7 @@ add_task(async function test_content_process_engagement_that_changes_page() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -319,6 +326,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -344,6 +352,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -369,6 +378,7 @@ add_task(
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -417,6 +427,7 @@ add_task(async function test_unload_listeners_single_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -476,6 +487,7 @@ add_task(async function test_unload_listeners_multi_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -501,6 +513,7 @@ add_task(async function test_unload_listeners_multi_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_provider.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_provider.js
index 1e44957daa..d3b064ffbf 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_provider.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_provider.js
@@ -55,6 +55,7 @@ add_task(async function test_load_serps_and_click_organic() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -80,6 +81,7 @@ add_task(async function test_load_serps_and_click_organic() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -141,6 +143,7 @@ add_task(async function test_load_serps_and_click_ads() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -166,6 +169,7 @@ add_task(async function test_load_serps_and_click_ads() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -223,6 +227,7 @@ add_task(async function test_load_serps_and_click_related() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -248,6 +253,7 @@ add_task(async function test_load_serps_and_click_related() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -273,6 +279,7 @@ add_task(async function test_load_serps_and_click_related() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -292,6 +299,7 @@ add_task(async function test_load_serps_and_click_related() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -353,6 +361,7 @@ add_task(async function test_load_pages_tabhistory() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -379,6 +388,7 @@ add_task(async function test_load_pages_tabhistory() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -404,6 +414,7 @@ add_task(async function test_load_pages_tabhistory() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -426,6 +437,7 @@ add_task(async function test_load_pages_tabhistory() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -448,6 +460,7 @@ add_task(async function test_load_pages_tabhistory() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -467,6 +480,7 @@ add_task(async function test_load_pages_tabhistory() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -489,6 +503,7 @@ add_task(async function test_load_pages_tabhistory() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -511,6 +526,7 @@ add_task(async function test_load_pages_tabhistory() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_tab.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_tab.js
index 478a995e97..b5e54eb6bc 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_tab.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_multi_tab.js
@@ -85,6 +85,7 @@ add_task(async function test_load_serps_and_click_related_searches() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -111,6 +112,7 @@ add_task(async function test_load_serps_and_click_related_searches() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -137,6 +139,7 @@ add_task(async function test_load_serps_and_click_related_searches() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -164,6 +167,7 @@ add_task(async function test_load_serps_and_click_related_searches() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -184,6 +188,7 @@ add_task(async function test_load_serps_and_click_related_searches() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -210,6 +215,7 @@ add_task(async function test_load_serps_and_click_related_searches() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -229,6 +235,7 @@ add_task(async function test_load_serps_and_click_related_searches() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -249,6 +256,7 @@ add_task(async function test_load_serps_and_click_related_searches() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [],
},
@@ -262,6 +270,7 @@ add_task(async function test_load_serps_and_click_related_searches() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [],
},
@@ -317,6 +326,7 @@ add_task(async function test_different_sources_click_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -337,6 +347,7 @@ add_task(async function test_different_sources_click_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -363,6 +374,7 @@ add_task(async function test_different_sources_click_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -386,6 +398,7 @@ add_task(async function test_different_sources_click_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -450,6 +463,7 @@ add_task(async function test_different_sources_click_redirect_ad_in_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -470,6 +484,7 @@ add_task(async function test_different_sources_click_redirect_ad_in_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -496,6 +511,7 @@ add_task(async function test_different_sources_click_redirect_ad_in_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -519,6 +535,7 @@ add_task(async function test_different_sources_click_redirect_ad_in_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -574,6 +591,7 @@ add_task(async function test_update_query_params_after_search() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -600,6 +618,7 @@ add_task(async function test_update_query_params_after_search() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -620,6 +639,7 @@ add_task(async function test_update_query_params_after_search() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -673,6 +693,7 @@ add_task(async function test_update_query_params() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -699,6 +720,7 @@ add_task(async function test_update_query_params() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -719,6 +741,7 @@ add_task(async function test_update_query_params() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -779,6 +802,7 @@ add_task(async function test_update_query_params_multiple_related() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -805,6 +829,7 @@ add_task(async function test_update_query_params_multiple_related() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -825,6 +850,7 @@ add_task(async function test_update_query_params_multiple_related() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -851,6 +877,7 @@ add_task(async function test_update_query_params_multiple_related() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_single_tab.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_single_tab.js
index 4f85c6cfa1..360e531c14 100644
--- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_single_tab.js
+++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_spa_single_tab.js
@@ -37,6 +37,7 @@ add_task(async function test_load_serp() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -95,6 +96,7 @@ add_task(async function test_load_serp_and_push_unrelated_state() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -134,6 +136,7 @@ add_task(async function test_load_serp_and_load_non_serp_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -176,6 +179,7 @@ add_task(async function test_load_serp_and_click_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -207,6 +211,7 @@ add_task(async function test_load_serp_and_click_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -251,6 +256,7 @@ add_task(async function test_load_serp_and_click_redirect_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -294,6 +300,7 @@ add_task(async function test_load_serp_and_click_redirect_ad_in_new_tab() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -338,6 +345,7 @@ add_task(async function test_load_serp_click_a_related_search() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -363,6 +371,7 @@ add_task(async function test_load_serp_click_a_related_search() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -401,6 +410,7 @@ add_task(async function test_load_serp_click_a_related_search_click_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -426,6 +436,7 @@ add_task(async function test_load_serp_click_a_related_search_click_ad() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -470,6 +481,7 @@ add_task(async function test_load_serp_click_non_serp_tab_click_all() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -509,6 +521,7 @@ add_task(async function test_load_serp_click_non_serp_tab_click_all() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -534,6 +547,7 @@ add_task(async function test_load_serp_click_non_serp_tab_click_all() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -576,6 +590,7 @@ add_task(async function test_load_serp_and_use_back_and_forward() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
engagements: [
{
@@ -601,6 +616,7 @@ add_task(async function test_load_serp_and_use_back_and_forward() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -623,6 +639,7 @@ add_task(async function test_load_serp_and_use_back_and_forward() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
@@ -645,6 +662,7 @@ add_task(async function test_load_serp_and_use_back_and_forward() {
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
+ is_signed_in: "false",
},
adImpressions: [
{
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..0ad2095b30
--- /dev/null
+++ b/browser/components/search/test/marionette/telemetry/test_ping_submitted.py
@@ -0,0 +1,91 @@
+# -*- 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_hidden: "0",
+ num_ads_loaded: "2",
+ 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
new file mode 100644
index 0000000000..b234246cac
--- /dev/null
+++ b/browser/components/search/test/unit/corruptDB.sqlite
Binary files differ
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..17fb802085
--- /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"],
+];
+
+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)
+ );
+
+ checkUISchemaValid(schemaData, uiSchemaData);
+ }
+});
diff --git a/browser/components/search/test/unit/test_urlTelemetry_generic.js b/browser/components/search/test/unit/test_urlTelemetry_generic.js
index e967002421..556e167681 100644
--- a/browser/components/search/test/unit/test_urlTelemetry_generic.js
+++ b/browser/components/search/test/unit/test_urlTelemetry_generic.js
@@ -7,7 +7,6 @@ ChromeUtils.defineESModuleGetters(this, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs",
SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs",
- SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
sinon: "resource://testing-common/Sinon.sys.mjs",
});
@@ -64,10 +63,11 @@ const TESTS = [
provider: "example",
tagged: "true",
partner_code: "ff",
+ source: "unknown",
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
- source: "unknown",
+ is_signed_in: "false",
},
},
{
@@ -81,10 +81,11 @@ const TESTS = [
provider: "example",
tagged: "true",
partner_code: "ff",
+ source: "unknown",
is_shopping_page: "true",
is_private: "false",
shopping_tab_displayed: "false",
- source: "unknown",
+ is_signed_in: "false",
},
},
{
@@ -98,10 +99,11 @@ const TESTS = [
provider: "example",
tagged: "true",
partner_code: "tb",
+ source: "unknown",
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
- source: "unknown",
+ is_signed_in: "false",
},
},
{
@@ -115,10 +117,11 @@ const TESTS = [
provider: "example",
tagged: "false",
partner_code: "foo",
+ source: "unknown",
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
- source: "unknown",
+ is_signed_in: "false",
},
},
{
@@ -132,10 +135,11 @@ const TESTS = [
provider: "example",
tagged: "false",
partner_code: "other",
+ source: "unknown",
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
- source: "unknown",
+ is_signed_in: "false",
},
},
{
@@ -149,10 +153,11 @@ const TESTS = [
provider: "example",
tagged: "false",
partner_code: "other",
+ source: "unknown",
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
- source: "unknown",
+ is_signed_in: "false",
},
},
{
@@ -166,10 +171,11 @@ const TESTS = [
provider: "example",
tagged: "false",
partner_code: "",
+ source: "unknown",
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
- source: "unknown",
+ is_signed_in: "false",
},
},
{
@@ -183,10 +189,11 @@ const TESTS = [
provider: "example",
tagged: "false",
partner_code: "",
+ source: "unknown",
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
- source: "unknown",
+ is_signed_in: "false",
},
},
{
@@ -200,10 +207,11 @@ const TESTS = [
provider: "example2",
tagged: "false",
partner_code: "",
+ source: "unknown",
is_shopping_page: "false",
is_private: "false",
shopping_tab_displayed: "false",
- source: "unknown",
+ is_signed_in: "false",
},
},
];
@@ -259,10 +267,6 @@ async function testAdUrlClicked(serpUrl, adUrl, expectedAdKey) {
do_get_profile();
add_task(async function setup() {
- Services.prefs.setBoolPref(
- SearchUtils.BROWSER_SEARCH_PREF + "serpEventTelemetry.enabled",
- true
- );
Services.fog.initializeFOG();
await SearchSERPTelemetry.init();
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
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..ebb2a66a53 100644
--- a/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs
+++ b/browser/components/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs
@@ -182,10 +182,10 @@ export var RecentlyClosedTabsAndWindowsMenuUtils = {
* @param aEvent
* The command event when the user clicks the restore all menu item
*/
- onRestoreAllWindowsCommand(aEvent) {
- const count = lazy.SessionStore.getClosedWindowCount();
- for (let index = 0; index < count; index++) {
- lazy.SessionStore.undoCloseWindow(index);
+ onRestoreAllWindowsCommand() {
+ const closedData = lazy.SessionStore.getClosedWindowData();
+ for (const { closedId } of closedData) {
+ lazy.SessionStore.undoCloseById(closedId);
}
},
@@ -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..a44a662e07 100644
--- a/browser/components/sessionstore/SessionFile.sys.mjs
+++ b/browser/components/sessionstore/SessionFile.sys.mjs
@@ -18,6 +18,7 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs",
@@ -194,6 +195,7 @@ var SessionFileInternal = {
},
async _readInternal(useOldExtension) {
+ Services.telemetry.setEventRecordingEnabled("session_restore", true);
let result;
let noFilesFound = true;
this._usingOldExtension = useOldExtension;
@@ -233,7 +235,7 @@ var SessionFileInternal = {
// 1546847. Just in case there are problems in the format of
// the parsed data, continue on. Favicons might be broken, but
// the session will at least be recovered
- console.error(e);
+ lazy.sessionStoreLogger.error(e);
}
}
@@ -246,11 +248,23 @@ var SessionFileInternal = {
)
) {
// Skip sessionstore files that we don't understand.
- console.error(
+ lazy.sessionStoreLogger.warn(
"Cannot extract data from Session Restore file ",
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,32 +273,84 @@ 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);
Services.telemetry
.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS")
.add(Date.now() - startMs);
+ lazy.sessionStoreLogger.debug(`Successful file read of ${key} file`);
break;
} 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.",
+ }
+ );
+ // A file not existing can be normal and expected.
+ lazy.sessionStoreLogger.debug(
+ `Can't read session file which doesn't exist: ${key}`
+ );
} else if (
DOMException.isInstance(ex) &&
ex.name == "NotAllowedError"
) {
// The file might be inaccessible due to wrong permissions
// or similar failures. We'll just count it as "corrupted".
- console.error("Could not read session file ", ex);
+ lazy.sessionStoreLogger.error(
+ `NotAllowedError when reading session file: ${key}`,
+ 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(
+ lazy.sessionStoreLogger.error(
"Corrupt session file (invalid JSON found) ",
ex,
ex.stack
);
// 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 +358,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",
+ }
+ );
}
}
}
@@ -317,6 +394,9 @@ var SessionFileInternal = {
if (!result) {
// If everything fails, start with an empty session.
+ lazy.sessionStoreLogger.warn(
+ "No readable session files found to restore, starting with empty session"
+ );
result = {
origin: "empty",
source: "",
diff --git a/browser/components/sessionstore/SessionLogger.sys.mjs b/browser/components/sessionstore/SessionLogger.sys.mjs
new file mode 100644
index 0000000000..a7c99f911e
--- /dev/null
+++ b/browser/components/sessionstore/SessionLogger.sys.mjs
@@ -0,0 +1,87 @@
+/* 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 { LogManager } from "resource://gre/modules/LogManager.sys.mjs";
+// See Bug 1889052
+// eslint-disable-next-line mozilla/use-console-createInstance
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
+ cancelIdleCallback: "resource://gre/modules/Timer.sys.mjs",
+ requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
+});
+
+const loggerNames = ["SessionStore"];
+
+export const sessionStoreLogger = Log.repository.getLogger("SessionStore");
+sessionStoreLogger.manageLevelFromPref("browser.sessionstore.loglevel");
+
+class SessionLogManager extends LogManager {
+ #idleCallbackId = null;
+ #observers = new Set();
+
+ QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]);
+
+ constructor(options = {}) {
+ super(options);
+
+ Services.obs.addObserver(this, "sessionstore-windows-restored");
+ this.#observers.add("sessionstore-windows-restored");
+
+ lazy.AsyncShutdown.profileBeforeChange.addBlocker(
+ "SessionLogManager: finalize and flush any logs to disk",
+ () => {
+ return this.stop();
+ }
+ );
+ }
+
+ async stop() {
+ if (this.#observers.has("sessionstore-windows-restored")) {
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ this.#observers.delete("sessionstore-windows-restored");
+ }
+ await this.requestLogFlush(true);
+ this.finalize();
+ }
+
+ observe(subject, topic, _) {
+ switch (topic) {
+ case "sessionstore-windows-restored":
+ // this represents the moment session restore is nominally complete
+ // and is a good time to ensure any log messages are flushed to disk
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ this.#observers.delete("sessionstore-windows-restored");
+ this.requestLogFlush();
+ break;
+ }
+ }
+
+ async requestLogFlush(immediate = false) {
+ if (this.#idleCallbackId && !immediate) {
+ return;
+ }
+ if (this.#idleCallbackId) {
+ lazy.cancelIdleCallback(this.#idleCallbackId);
+ this.#idleCallbackId = null;
+ }
+ if (!immediate) {
+ await new Promise(resolve => {
+ this.#idleCallbackId = lazy.requestIdleCallback(resolve);
+ });
+ this.#idleCallbackId = null;
+ }
+ await this.resetFileLog();
+ }
+}
+
+export const logManager = new SessionLogManager({
+ prefRoot: "browser.sessionstore.",
+ logNames: loggerNames,
+ logFilePrefix: "sessionrestore",
+ logFileSubDirectoryEntries: ["sessionstore-logs"],
+ testTopicPrefix: "sessionrestore:log-manager:",
+});
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..72df4316e9 100644
--- a/browser/components/sessionstore/SessionStartup.sys.mjs
+++ b/browser/components/sessionstore/SessionStartup.sys.mjs
@@ -38,6 +38,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
StartupPerformance:
"resource:///modules/sessionstore/StartupPerformance.sys.mjs",
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
});
const STATE_RUNNING_STR = "running";
@@ -50,32 +51,7 @@ const TYPE_DEFER_SESSION = 3;
// 'browser.startup.page' preference value to resume the previous session.
const BROWSER_STARTUP_RESUME_SESSION = 3;
-function warning(msg, exception) {
- let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(
- Ci.nsIScriptError
- );
- consoleMsg.init(
- msg,
- exception.fileName,
- null,
- exception.lineNumber,
- 0,
- Ci.nsIScriptError.warningFlag,
- "component javascript"
- );
- Services.console.logMessage(consoleMsg);
-}
-
-var gOnceInitializedDeferred = (function () {
- let deferred = {};
-
- deferred.promise = new Promise((resolve, reject) => {
- deferred.resolve = resolve;
- deferred.reject = reject;
- });
-
- return deferred;
-})();
+var gOnceInitializedDeferred = Promise.withResolvers();
/* :::::::: The Service ::::::::::::::: */
@@ -119,6 +95,7 @@ export var SessionStartup = {
"browser.sessionstore.resuming_after_os_restart"
)
) {
+ lazy.sessionStoreLogger.debug("resuming_after_os_restart");
if (!Services.appinfo.restartedByOS) {
// We had set resume_session_once in order to resume after an OS restart,
// but we aren't automatically started by the OS (or else appinfo.restartedByOS
@@ -136,8 +113,17 @@ export var SessionStartup = {
}
lazy.SessionFile.read().then(
- this._onSessionFileRead.bind(this),
- console.error
+ result => {
+ lazy.sessionStoreLogger.debug(
+ `Completed SessionFile.read() with result.origin: ${result.origin}`
+ );
+ return this._onSessionFileRead(result);
+ },
+ err => {
+ // SessionFile.read catches most expected failures,
+ // so a promise rejection here should be logged as an error
+ lazy.sessionStoreLogger.error("Failure from _onSessionFileRead", err);
+ }
);
},
@@ -158,6 +144,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);
@@ -169,12 +160,18 @@ export var SessionStartup = {
if (stateString != source) {
// The session has been modified by an add-on, reparse.
+ lazy.sessionStoreLogger.debug(
+ "After sessionstore-state-read, session has been modified"
+ );
try {
this._initialState = JSON.parse(stateString);
} catch (ex) {
// That's not very good, an add-on has rewritten the initial
// state to something that won't parse.
- warning("Observer rewrote the state to something that won't parse", ex);
+ lazy.sessionStoreLogger.error(
+ "'sessionstore-state-read' observer rewrote the state to something that won't parse",
+ ex
+ );
}
} else {
// No need to reparse
@@ -184,6 +181,7 @@ export var SessionStartup = {
if (this._initialState == null) {
// No valid session found.
this._sessionType = this.NO_SESSION;
+ lazy.sessionStoreLogger.debug("No valid session found");
Services.obs.notifyObservers(null, "sessionstore-state-finalized");
gOnceInitializedDeferred.resolve();
return;
@@ -199,23 +197,38 @@ export var SessionStartup = {
}, 0)
);
}, 0);
+ lazy.sessionStoreLogger.debug(
+ `initialState contains ${pinnedTabCount} pinned tabs`
+ );
Services.telemetry.scalarSetMaximum(
"browser.engagement.max_concurrent_tab_pinned_count",
pinnedTabCount
);
}, 60000);
+ let isAutomaticRestoreEnabled = this.isAutomaticRestoreEnabled();
+ lazy.sessionStoreLogger.debug(
+ `isAutomaticRestoreEnabled: ${isAutomaticRestoreEnabled}`
+ );
// If this is a normal restore then throw away any previous session.
- if (!this.isAutomaticRestoreEnabled() && this._initialState) {
+ if (!isAutomaticRestoreEnabled && this._initialState) {
+ lazy.sessionStoreLogger.debug(
+ "Discarding previous session as we have initialState"
+ );
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 +254,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,10 +269,24 @@ 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,
+ }
+ );
+ lazy.sessionStoreLogger.debug(
+ `Previous shutdown ok? ${this._previousSessionCrashed}, reason: ${previousSessionCrashedReason}`
+ );
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
if (this.sessionType == this.NO_SESSION) {
+ lazy.sessionStoreLogger.debug("Will restore no session");
this._initialState = null; // Reset the state.
} else {
Services.obs.addObserver(this, "browser:purge-session-history", true);
@@ -268,10 +302,11 @@ 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");
+ lazy.sessionStoreLogger.debug(`sessionstore-windows-restored`);
// Free _initialState after nsSessionStore is done with it.
this._initialState = null;
this._didRestore = true;
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
index f269251f54..7bd262fe09 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",
@@ -222,12 +169,15 @@ ChromeUtils.defineESModuleGetters(lazy, {
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
HomePage: "resource:///modules/HomePage.sys.mjs",
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
SessionCookies: "resource:///modules/sessionstore/SessionCookies.sys.mjs",
SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs",
SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
+ SessionStoreHelper:
+ "resource://gre/modules/sessionstore/SessionStoreHelper.sys.mjs",
TabAttributes: "resource:///modules/sessionstore/TabAttributes.sys.mjs",
TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs",
TabState: "resource:///modules/sessionstore/TabState.sys.mjs",
@@ -258,6 +208,9 @@ var gResistFingerprintingEnabled = false;
* @namespace SessionStore
*/
export var SessionStore = {
+ get logger() {
+ return SessionStoreInternal._log;
+ },
get promiseInitialized() {
return SessionStoreInternal.promiseInitialized;
},
@@ -645,10 +598,6 @@ export var SessionStore = {
SessionStoreInternal.deleteCustomGlobalValue(aKey);
},
- persistTabAttribute: function ss_persistTabAttribute(aName) {
- SessionStoreInternal.persistTabAttribute(aName);
- },
-
restoreLastSession: function ss_restoreLastSession() {
SessionStoreInternal.restoreLastSession();
},
@@ -813,18 +762,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.
*/
@@ -1115,6 +1052,10 @@ var SessionStoreInternal = {
Services.telemetry
.getHistogramById("FX_SESSION_RESTORE_PRIVACY_LEVEL")
.add(Services.prefs.getIntPref("browser.sessionstore.privacy_level"));
+
+ this.promiseAllWindowsRestored.finally(() => () => {
+ this._log.debug("promiseAllWindowsRestored finalized");
+ });
},
/**
@@ -1124,10 +1065,13 @@ var SessionStoreInternal = {
TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS");
let state;
let ss = lazy.SessionStartup;
-
- if (ss.willRestore() || ss.sessionType == ss.DEFER_SESSION) {
+ let willRestore = ss.willRestore();
+ if (willRestore || ss.sessionType == ss.DEFER_SESSION) {
state = ss.state;
}
+ this._log.debug(
+ `initSession willRestore: ${willRestore}, SessionStartup.sessionType: ${ss.sessionType}`
+ );
if (state) {
try {
@@ -1143,6 +1087,9 @@ var SessionStoreInternal = {
} else {
state = null;
}
+ this._log.debug(
+ `initSession deferred restore with ${iniState.windows.length} initial windows, ${remainingState.windows.length} remaining windows`
+ );
if (remainingState.windows.length) {
LastSession.setState(remainingState);
@@ -1161,6 +1108,9 @@ var SessionStoreInternal = {
if (restoreAsCrashed) {
this._recentCrashes =
((state.session && state.session.recentCrashes) || 0) + 1;
+ this._log.debug(
+ `initSession, restoreAsCrashed, crashes: ${this._recentCrashes}`
+ );
// _needsRestorePage will record sessionrestore_interstitial,
// including the specific reason we decided we needed to show
@@ -1175,9 +1125,11 @@ var SessionStoreInternal = {
lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
};
state = { windows: [{ tabs: [{ entries: [entry], formdata }] }] };
+ this._log.debug("initSession, will show about:sessionrestore");
} else if (
this._hasSingleTabWithURL(state.windows, "about:welcomeback")
) {
+ this._log.debug("initSession, will show about:welcomeback");
Services.telemetry.keyedScalarAdd(
"browser.engagement.sessionrestore_interstitial",
"shown_only_about_welcomeback",
@@ -1200,7 +1152,7 @@ var SessionStoreInternal = {
"autorestore",
1
);
-
+ this._log.debug("initSession, will autorestore");
this._removeExplicitlyClosedTabs(state);
}
@@ -1228,7 +1180,7 @@ var SessionStoreInternal = {
state?.windows?.forEach(win => delete win._maybeDontRestoreTabs);
state?._closedWindows?.forEach(win => delete win._maybeDontRestoreTabs);
} catch (ex) {
- this._log.error("The session file is invalid: " + ex);
+ this._log.error("The session file is invalid: ", ex);
}
}
@@ -1312,10 +1264,7 @@ var SessionStoreInternal = {
gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
});
- this._log = console.createInstance({
- prefix: "SessionStore",
- maxLogLevel: gDebuggingEnabled ? "Debug" : "Warn",
- });
+ this._log = lazy.sessionStoreLogger;
this._max_tabs_undo = this._prefBranch.getIntPref(
"sessionstore.max_tabs_undo"
@@ -1354,8 +1303,6 @@ var SessionStoreInternal = {
"privacy.resistFingerprinting"
);
Services.prefs.addObserver("privacy.resistFingerprinting", this);
-
- this._shistoryInParent = Services.appinfo.sessionHistoryInParent;
},
/**
@@ -1434,43 +1381,48 @@ 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.top && aSubject.isContent) {
+ const permanentKey = aSubject.embedderElement?.permanentKey;
+ if (permanentKey) {
+ this.maybeRecreateSHistoryListener(permanentKey, aSubject);
}
}
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;
}
},
- getOrCreateSHistoryListener(
- permanentKey,
- browsingContext,
- collectImmediately = false
- ) {
+ getOrCreateSHistoryListener(permanentKey, browsingContext) {
+ if (!permanentKey || browsingContext !== browsingContext.top) {
+ return null;
+ }
+
+ const listener = this._browserSHistoryListener.get(permanentKey);
+ if (listener) {
+ return listener;
+ }
+
+ return this.createSHistoryListener(permanentKey, browsingContext, false);
+ },
+
+ maybeRecreateSHistoryListener(permanentKey, browsingContext) {
+ const listener = this._browserSHistoryListener.get(permanentKey);
+ if (!listener || listener._browserId != browsingContext.browserId) {
+ listener?.unregister(permanentKey);
+ this.createSHistoryListener(permanentKey, browsingContext, true);
+ }
+ },
+
+ createSHistoryListener(permanentKey, browsingContext, collectImmediately) {
class SHistoryListener {
constructor() {
this.QueryInterface = ChromeUtils.generateQI([
@@ -1573,25 +1525,12 @@ var SessionStoreInternal = {
}
}
- if (!Services.appinfo.sessionHistoryInParent) {
- throw new Error("This function should only be used with SHIP");
- }
-
- if (!permanentKey || browsingContext !== browsingContext.top) {
- return null;
- }
-
let sessionHistory = browsingContext.sessionHistory;
if (!sessionHistory) {
return null;
}
- let listener = this._browserSHistoryListener.get(permanentKey);
- if (listener) {
- return listener;
- }
-
- listener = new SHistoryListener();
+ const listener = new SHistoryListener();
sessionHistory.addSHistoryListener(listener);
this._browserSHistoryListener.set(permanentKey, listener);
@@ -1691,29 +1630,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 +1661,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 +1762,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: [],
@@ -1995,6 +1825,9 @@ var SessionStoreInternal = {
lazy.SessionSaver.updateLastSaveTime();
if (isPrivateWindow) {
+ this._log.debug(
+ "initializeWindow, the window is private. Saving SessionStartup.state for possibly restoring later"
+ );
// We're starting with a single private window. Save the state we
// actually wanted to restore so that we can do it later in case
// the user opens another, non-private window.
@@ -2092,7 +1925,7 @@ var SessionStoreInternal = {
windows: [closedWindowState],
});
- // These are our pinned tabs, which we should restore
+ // These are our pinned tabs and sidebar attributes, which we should restore
if (appTabsState.windows.length) {
newWindowState = appTabsState.windows[0];
delete newWindowState.__lastSessionWindowID;
@@ -2152,6 +1985,9 @@ var SessionStoreInternal = {
// Just call initializeWindow() directly if we're initialized already.
if (this._sessionInitialized) {
+ this._log.debug(
+ "onBeforeBrowserWindowShown, session already initialized, initializing window"
+ );
this.initializeWindow(aWindow);
return;
}
@@ -2187,6 +2023,9 @@ var SessionStoreInternal = {
this._promiseReadyForInitialization
.then(() => {
if (aWindow.closed) {
+ this._log.debug(
+ "When _promiseReadyForInitialization resolved, the window was closed"
+ );
return;
}
@@ -2211,7 +2050,12 @@ var SessionStoreInternal = {
this._deferredInitialized.resolve();
}
})
- .catch(console.error);
+ .catch(ex => {
+ this._log.error(
+ "Exception when handling _promiseReadyForInitialization resolution:",
+ ex
+ );
+ });
},
/**
@@ -2347,7 +2191,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 +2246,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 +2267,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 +2328,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 +3479,7 @@ var SessionStoreInternal = {
}
// Create a new tab.
- let userContextId = aTab.getAttribute("usercontextid");
+ let userContextId = aTab.getAttribute("usercontextid") || "";
let tabOptions = {
userContextId,
@@ -4273,12 +4108,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.
@@ -4732,12 +4561,16 @@ var SessionStoreInternal = {
}
let sidebarBox = aWindow.document.getElementById("sidebar-box");
- let sidebar = sidebarBox.getAttribute("sidebarcommand");
- if (sidebar && sidebarBox.getAttribute("checked") == "true") {
- winData.sidebar = sidebar;
- } else if (winData.sidebar) {
- delete winData.sidebar;
+ let command = sidebarBox.getAttribute("sidebarcommand");
+ if (command && sidebarBox.getAttribute("checked") == "true") {
+ winData.sidebar = {
+ command,
+ positionEnd: sidebarBox.getAttribute("positionend"),
+ };
+ } else if (winData.sidebar?.command) {
+ delete winData.sidebar.command;
}
+
let workspaceID = aWindow.getWorkspaceID();
if (workspaceID) {
winData.workspaceID = workspaceID;
@@ -4961,6 +4794,7 @@ var SessionStoreInternal = {
let windowsOpened = [];
for (let winData of root.windows) {
if (!winData || !winData.tabs || !winData.tabs[0]) {
+ this._log.debug(`_openWindows, skipping window with no tabs data`);
this._restoreCount--;
continue;
}
@@ -5005,6 +4839,8 @@ var SessionStoreInternal = {
let overwriteTabs = aOptions && aOptions.overwriteTabs;
let firstWindow = aOptions && aOptions.firstWindow;
+ this.restoreSidebar(aWindow, winData.sidebar);
+
// initialize window if necessary
if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi])) {
this.onLoad(aWindow);
@@ -5018,6 +4854,7 @@ var SessionStoreInternal = {
this._setWindowStateBusy(aWindow);
if (winData.workspaceID) {
+ this._log.debug(`Moving window to workspace: ${winData.workspaceID}`);
aWindow.moveToWorkspace(winData.workspaceID);
}
@@ -5070,12 +4907,18 @@ var SessionStoreInternal = {
this._prefBranch.getBoolPref("sessionstore.restore_tabs_lazily") &&
this._restore_on_demand;
+ this._log.debug(
+ `restoreWindow, will restore ${winData.tabs.length} tabs, restoreTabsLazily: ${restoreTabsLazily}`
+ );
if (winData.tabs.length) {
var tabs = tabbrowser.createTabsForSessionRestore(
restoreTabsLazily,
selectTab,
winData.tabs
);
+ this._log.debug(
+ `restoreWindow, createTabsForSessionRestore returned {tabs.length} tabs`
+ );
}
// Move the originally open tabs to the end.
@@ -5297,6 +5140,7 @@ var SessionStoreInternal = {
root = typeof aState == "string" ? JSON.parse(aState) : aState;
} catch (ex) {
// invalid state object - don't restore anything
+ this._log.debug(`restoreWindows failed to parse ${typeof aState} state`);
this._log.error(ex);
this._sendRestoreCompletedNotifications();
return;
@@ -5315,9 +5159,13 @@ var SessionStoreInternal = {
);
}
}
+ this._log.debug(`Restored ${this._closedWindows.length} closed windows`);
this._closedObjectsChanged = true;
}
+ this._log.debug(
+ `restoreWindows will restore ${root.windows?.length} windows`
+ );
// We're done here if there are no windows.
if (!root.windows || !root.windows.length) {
this._sendRestoreCompletedNotifications();
@@ -5440,7 +5288,7 @@ var SessionStoreInternal = {
let browser = tab.linkedBrowser;
if (TAB_STATE_FOR_BROWSER.has(browser)) {
- console.error("Must reset tab before calling restoreTab.");
+ this._log.warn("Must reset tab before calling restoreTab.");
return;
}
@@ -5480,13 +5328,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 +5497,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 +5504,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,
});
@@ -5790,13 +5603,34 @@ var SessionStoreInternal = {
"screenX" in aWinData ? +aWinData.screenX : NaN,
"screenY" in aWinData ? +aWinData.screenY : NaN,
aWinData.sizemode || "",
- aWinData.sizemodeBeforeMinimized || "",
- aWinData.sidebar || ""
+ aWinData.sizemodeBeforeMinimized || ""
);
+ this.restoreSidebar(aWindow, aWinData.sidebar);
}, 0);
},
/**
+ * @param aWindow
+ * Window reference
+ * @param aSidebar
+ * Object containing command (sidebarcommand/category) and
+ * positionEnd (reflecting the sidebar.position_start pref)
+ */
+ restoreSidebar(aWindow, aSidebar) {
+ let sidebarBox = aWindow.document.getElementById("sidebar-box");
+ if (
+ aSidebar?.command &&
+ (sidebarBox.getAttribute("sidebarcommand") != aSidebar.command ||
+ !sidebarBox.getAttribute("checked"))
+ ) {
+ aWindow.SidebarController.showInitially(aSidebar.command);
+ if (aSidebar?.positionEnd) {
+ sidebarBox.setAttribute("positionend", "");
+ }
+ }
+ },
+
+ /**
* Restore a window's dimensions
* @param aWidth
* Window width in desktop pixels
@@ -5810,8 +5644,6 @@ var SessionStoreInternal = {
* Window size mode (eg: maximized)
* @param aSizeModeBeforeMinimized
* Window size mode before window got minimized (eg: maximized)
- * @param aSidebar
- * Sidebar command
*/
restoreDimensions: function ssi_restoreDimensions(
aWindow,
@@ -5820,8 +5652,7 @@ var SessionStoreInternal = {
aLeft,
aTop,
aSizeMode,
- aSizeModeBeforeMinimized,
- aSidebar
+ aSizeModeBeforeMinimized
) {
var win = aWindow;
var _this = this;
@@ -5963,14 +5794,6 @@ var SessionStoreInternal = {
break;
}
}
- let sidebarBox = aWindow.document.getElementById("sidebar-box");
- if (
- aSidebar &&
- (sidebarBox.getAttribute("sidebarcommand") != aSidebar ||
- !sidebarBox.getAttribute("checked"))
- ) {
- aWindow.SidebarUI.showInitially(aSidebar);
- }
// since resizing/moving a window brings it to the foreground,
// we might want to re-focus the last focused window
if (this.windowToFocus) {
@@ -6213,6 +6036,11 @@ var SessionStoreInternal = {
features.push("private");
}
+ this._log.debug(
+ `Opening window with features: ${features.join(
+ ","
+ )}, argString: ${argString}.`
+ );
var window = Services.ww.openWindow(
null,
AppConstants.BROWSER_CHROME_URL,
@@ -6492,6 +6320,15 @@ var SessionStoreInternal = {
if (PERSIST_SESSIONS) {
newWindowState._closedTabs = Cu.cloneInto(window._closedTabs, {});
}
+
+ // We want to preserve the sidebar if previously open in the window
+ if (window.sidebar?.command) {
+ newWindowState.sidebar = {
+ command: window.sidebar.command,
+ positionEnd: !!window.sidebar.positionEnd,
+ };
+ }
+
for (let tIndex = 0; tIndex < window.tabs.length; ) {
if (window.tabs[tIndex].pinned) {
// Adjust window.selected
@@ -6624,6 +6461,7 @@ var SessionStoreInternal = {
// This was the last window restored at startup, notify observers.
if (!this._browserSetState) {
Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
+ this._log.debug(`All ${this._restoreCount} windows restored`);
this._deferredAllWindowsRestored.resolve();
} else {
// _browserSetState is used only by tests, and it uses an alternate
@@ -6828,10 +6666,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 +6691,6 @@ var SessionStoreInternal = {
return;
}
- if (!Services.appinfo.sessionHistoryInParent) {
- browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
- }
this._resetLocalTabRestoringState(tab);
},
@@ -6937,98 +6770,6 @@ var SessionStoreInternal = {
return deferred;
},
- /**
- * Builds a single nsISessionStoreRestoreData tree for the provided |formdata|
- * and |scroll| trees.
- */
- buildRestoreData(formdata, scroll) {
- function addFormEntries(root, fields, isXpath) {
- for (let [key, value] of Object.entries(fields)) {
- switch (typeof value) {
- case "string":
- root.addTextField(isXpath, key, value);
- break;
- case "boolean":
- root.addCheckbox(isXpath, key, value);
- break;
- case "object": {
- if (value === null) {
- break;
- }
- if (
- value.hasOwnProperty("type") &&
- value.hasOwnProperty("fileList")
- ) {
- root.addFileList(isXpath, key, value.type, value.fileList);
- break;
- }
- if (
- value.hasOwnProperty("selectedIndex") &&
- value.hasOwnProperty("value")
- ) {
- root.addSingleSelect(
- isXpath,
- key,
- value.selectedIndex,
- value.value
- );
- break;
- }
- if (
- value.hasOwnProperty("value") &&
- value.hasOwnProperty("state")
- ) {
- root.addCustomElement(isXpath, key, value.value, value.state);
- break;
- }
- if (
- key === "sessionData" &&
- ["about:sessionrestore", "about:welcomeback"].includes(
- formdata.url
- )
- ) {
- root.addTextField(isXpath, key, JSON.stringify(value));
- break;
- }
- if (Array.isArray(value)) {
- root.addMultipleSelect(isXpath, key, value);
- break;
- }
- }
- }
- }
- }
-
- let root = SessionStoreUtils.constructSessionStoreRestoreData();
- if (scroll?.hasOwnProperty("scroll")) {
- root.scroll = scroll.scroll;
- }
- if (formdata?.hasOwnProperty("url")) {
- root.url = formdata.url;
- if (formdata.hasOwnProperty("innerHTML")) {
- // eslint-disable-next-line no-unsanitized/property
- root.innerHTML = formdata.innerHTML;
- }
- if (formdata.hasOwnProperty("xpath")) {
- addFormEntries(root, formdata.xpath, /* isXpath */ true);
- }
- if (formdata.hasOwnProperty("id")) {
- addFormEntries(root, formdata.id, /* isXpath */ false);
- }
- }
- let childrenLength = Math.max(
- scroll?.children?.length || 0,
- formdata?.children?.length || 0
- );
- for (let i = 0; i < childrenLength; i++) {
- root.addChild(
- this.buildRestoreData(formdata?.children?.[i], scroll?.children?.[i]),
- i
- );
- }
- return root;
- },
-
_waitForStateStop(browser, expectedURL = null) {
const deferred = Promise.withResolvers();
@@ -7048,7 +6789,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 +6850,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 +6884,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 +6927,7 @@ var SessionStoreInternal = {
this._tabStateRestorePromises.delete(browser.permanentKey);
- this._restoreHistoryComplete(browser, data);
+ this._restoreHistoryComplete(browser);
};
promise.then(onResolve).catch(() => {});
@@ -7206,7 +6943,10 @@ var SessionStoreInternal = {
if (!haveUserTypedValue && tabData.entries.length) {
return SessionStoreUtils.initializeRestore(
browser.browsingContext,
- this.buildRestoreData(tabData.formdata, tabData.scroll)
+ lazy.SessionStoreHelper.buildRestoreData(
+ tabData.formdata,
+ tabData.scroll
+ )
);
}
// Here, we need to load user data or about:blank instead.
@@ -7238,10 +6978,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 +7002,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 +7146,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 +7362,7 @@ var DirtyWindows = {
this._data.delete(window);
},
- clear(window) {
+ clear(_window) {
this._data = new WeakMap();
},
};
diff --git a/browser/components/sessionstore/SessionStoreFunctions.sys.mjs b/browser/components/sessionstore/SessionStoreFunctions.sys.mjs
new file mode 100644
index 0000000000..978c1a79cd
--- /dev/null
+++ b/browser/components/sessionstore/SessionStoreFunctions.sys.mjs
@@ -0,0 +1,93 @@
+/* -*- 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/. */
+import { SessionStore } from "resource:///modules/sessionstore/SessionStore.sys.mjs";
+
+export class SessionStoreFunctions {
+ UpdateSessionStore(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aCollectSHistory,
+ aData
+ ) {
+ return SessionStoreFuncInternal.updateSessionStore(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aCollectSHistory,
+ aData
+ );
+ }
+
+ UpdateSessionStoreForStorage(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aData
+ ) {
+ return SessionStoreFuncInternal.updateSessionStoreForStorage(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aData
+ );
+ }
+}
+
+var SessionStoreFuncInternal = {
+ updateSessionStore: function SSF_updateSessionStore(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aCollectSHistory,
+ aData
+ ) {
+ let { formdata, scroll } = aData;
+
+ if (formdata) {
+ aData.formdata = formdata.toJSON();
+ }
+
+ if (scroll) {
+ aData.scroll = scroll.toJSON();
+ }
+
+ SessionStore.updateSessionStoreFromTablistener(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ {
+ data: aData,
+ epoch: aEpoch,
+ sHistoryNeeded: aCollectSHistory,
+ }
+ );
+ },
+
+ updateSessionStoreForStorage: function SSF_updateSessionStoreForStorage(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ aEpoch,
+ aData
+ ) {
+ SessionStore.updateSessionStoreFromTablistener(
+ aBrowser,
+ aBrowsingContext,
+ aPermanentKey,
+ { data: { storage: aData }, epoch: aEpoch },
+ true
+ );
+ },
+};
+
+SessionStoreFunctions.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsISessionStoreFunctions",
+]);
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/components.conf b/browser/components/sessionstore/components.conf
new file mode 100644
index 0000000000..2776c4dcb7
--- /dev/null
+++ b/browser/components/sessionstore/components.conf
@@ -0,0 +1,12 @@
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{45ce6b2d-ffc8-4051-bb41-37ceeeb19e94}',
+ 'contract_ids': ['@mozilla.org/toolkit/sessionstore-functions;1'],
+ 'esModule': 'resource:///modules/sessionstore/SessionStoreFunctions.sys.mjs',
+ 'constructor': 'SessionStoreFunctions',
+ },
+]
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..97554aab31 100644
--- a/browser/components/sessionstore/moz.build
+++ b/browser/components/sessionstore/moz.build
@@ -5,23 +5,23 @@
# 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",
"SessionCookies.sys.mjs",
"SessionFile.sys.mjs",
+ "SessionLogger.sys.mjs",
"SessionMigration.sys.mjs",
"SessionSaver.sys.mjs",
"SessionStartup.sys.mjs",
"SessionStore.sys.mjs",
+ "SessionStoreFunctions.sys.mjs",
"SessionWriter.sys.mjs",
"StartupPerformance.sys.mjs",
"TabAttributes.sys.mjs",
@@ -30,6 +30,10 @@ EXTRA_JS_MODULES.sessionstore = [
"TabStateFlusher.sys.mjs",
]
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
TESTING_JS_MODULES += [
"test/SessionStoreTestUtils.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/sessionstore/test/marionette/manifest.toml b/browser/components/sessionstore/test/marionette/manifest.toml
index 6b62bea84e..0ff186778a 100644
--- a/browser/components/sessionstore/test/marionette/manifest.toml
+++ b/browser/components/sessionstore/test/marionette/manifest.toml
@@ -9,6 +9,10 @@ tags = "local"
["test_restore_manually_with_pinned_tabs.py"]
+["test_restore_sidebar_automatic.py"]
+
+["test_restore_sidebar.py"]
+
["test_restore_windows_after_close_last_tabs.py"]
skip-if = ["os == 'mac'"]
diff --git a/browser/components/sessionstore/test/marionette/test_restore_sidebar.py b/browser/components/sessionstore/test/marionette/test_restore_sidebar.py
new file mode 100644
index 0000000000..042b9f2b23
--- /dev/null
+++ b/browser/components/sessionstore/test/marionette/test_restore_sidebar.py
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 0.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/0.0/.
+
+import os
+import sys
+
+# add this directory to the path
+sys.path.append(os.path.dirname(__file__))
+
+from session_store_test_case import SessionStoreTestCase
+
+
+def inline(title):
+ return "data:text/html;charset=utf-8,<html><head><title>{}</title></head><body></body></html>".format(
+ title
+ )
+
+
+class TestSessionRestore(SessionStoreTestCase):
+ """
+ Test that the sidebar and its attributes are restored on reopening of window.
+ """
+
+ def setUp(self):
+ super(TestSessionRestore, self).setUp(
+ startup_page=1,
+ include_private=False,
+ restore_on_demand=True,
+ test_windows=set(
+ [
+ (
+ inline("lorem ipsom"),
+ inline("dolor"),
+ ),
+ ]
+ ),
+ )
+
+ def test_restore(self):
+ self.assertEqual(
+ len(self.marionette.chrome_window_handles),
+ 1,
+ msg="Should have 1 window open.",
+ )
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ window.SidebarController.show("viewHistorySidebar");
+ let sidebarBox = window.document.getElementById("sidebar-box")
+ sidebarBox.style.width = "100px";
+ """
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return !window.document.getElementById("sidebar-box").hidden;
+ """
+ ),
+ True,
+ "Sidebar is open before window is closed.",
+ )
+
+ self.marionette.restart()
+ self.marionette.set_context("chrome")
+
+ self.assertEqual(
+ len(self.marionette.chrome_window_handles),
+ 1,
+ msg="Windows from last session have been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return !window.document.getElementById("sidebar-box").hidden;
+ """
+ ),
+ True,
+ "Sidebar has been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return window.document.getElementById("sidebar-box").style.width;
+ """
+ ),
+ "100px",
+ "Sidebar width been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ const lazy = {};
+ ChromeUtils.defineESModuleGetters(lazy, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ });
+ let state = SessionStore.getCurrentState();
+ return state.windows[0].sidebar.command;
+ """
+ ),
+ "viewHistorySidebar",
+ "Correct sidebar category has been restored.",
+ )
diff --git a/browser/components/sessionstore/test/marionette/test_restore_sidebar_automatic.py b/browser/components/sessionstore/test/marionette/test_restore_sidebar_automatic.py
new file mode 100644
index 0000000000..58a9b93b47
--- /dev/null
+++ b/browser/components/sessionstore/test/marionette/test_restore_sidebar_automatic.py
@@ -0,0 +1,110 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 0.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/0.0/.
+
+import os
+import sys
+
+# add this directory to the path
+sys.path.append(os.path.dirname(__file__))
+
+from session_store_test_case import SessionStoreTestCase
+
+
+def inline(title):
+ return "data:text/html;charset=utf-8,<html><head><title>{}</title></head><body></body></html>".format(
+ title
+ )
+
+
+class TestSessionRestore(SessionStoreTestCase):
+ """
+ Test that the sidebar and its attributes are restored on reopening of window.
+ """
+
+ def setUp(self):
+ super(TestSessionRestore, self).setUp(
+ startup_page=3,
+ include_private=False,
+ restore_on_demand=False,
+ test_windows=set(
+ [
+ (
+ inline("lorem ipsom"),
+ inline("dolor"),
+ ),
+ ]
+ ),
+ )
+
+ def test_restore(self):
+ self.assertEqual(
+ len(self.marionette.chrome_window_handles),
+ 1,
+ msg="Should have 1 window open.",
+ )
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ window.SidebarController.show("viewHistorySidebar");
+ let sidebarBox = window.document.getElementById("sidebar-box")
+ sidebarBox.style.width = "100px";
+ """
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return !window.document.getElementById("sidebar-box").hidden;
+ """
+ ),
+ True,
+ "Sidebar is open before window is closed.",
+ )
+
+ self.marionette.restart()
+ self.marionette.set_context("chrome")
+
+ self.assertEqual(
+ len(self.marionette.chrome_window_handles),
+ 1,
+ msg="Windows from last session have been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return !window.document.getElementById("sidebar-box").hidden;
+ """
+ ),
+ True,
+ "Sidebar has been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ let window = BrowserWindowTracker.getTopWindow()
+ return window.document.getElementById("sidebar-box").style.width;
+ """
+ ),
+ "100px",
+ "Sidebar width been restored.",
+ )
+
+ self.assertEqual(
+ self.marionette.execute_script(
+ """
+ const lazy = {};
+ ChromeUtils.defineESModuleGetters(lazy, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ });
+ let state = SessionStore.getCurrentState();
+ return state.windows[0].sidebar.command;
+ """
+ ),
+ "viewHistorySidebar",
+ "Correct sidebar category has been restored.",
+ )
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/Windows11LimitedAccessFeatures.cpp b/browser/components/shell/Windows11LimitedAccessFeatures.cpp
new file mode 100644
index 0000000000..6bf20b6706
--- /dev/null
+++ b/browser/components/shell/Windows11LimitedAccessFeatures.cpp
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+/**
+ * This file exists so that LaunchModernSettingsDialogDefaultApps can be called
+ * without linking to libxul.
+ */
+#include "Windows11LimitedAccessFeatures.h"
+
+#include "mozilla/Logging.h"
+
+static mozilla::LazyLogModule sLog("Windows11LimitedAccessFeatures");
+
+#define LAF_LOG(level, msg, ...) MOZ_LOG(sLog, level, (msg, ##__VA_ARGS__))
+
+// MINGW32 is not supported for these features
+// Fall back function defined in the #else
+#ifndef __MINGW32__
+
+# include "nsString.h"
+# include "nsWindowsHelpers.h"
+
+# include "mozilla/Atomics.h"
+
+# include <wrl.h>
+# include <inspectable.h>
+# include <roapi.h>
+# include <windows.services.store.h>
+# include <windows.foundation.h>
+
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+using namespace ABI::Windows;
+using namespace ABI::Windows::Foundation;
+using namespace ABI::Windows::ApplicationModel;
+
+using namespace mozilla;
+
+/**
+ * To unlock features, we need:
+ * a feature identifier
+ * a token,
+ * an attestation string
+ * a token
+ *
+ * The token is generated by Microsoft and must
+ * match the publisher id Microsoft thinks we have, for a particular
+ * feature.
+ *
+ * To get a token, find the right microsoft email address by doing
+ * a search on the web for the feature you want unlocked and reach
+ * out to the right people at Microsoft.
+ *
+ * The token is generated from Microsoft.
+ * The jumbled code in the attestation string is a publisher id and
+ * must match the code in the resources / .rc file for the identity,
+ * looking like this for non-MSIX builds:
+ *
+ * Identity LimitedAccessFeature {{ L"MozillaFirefox_pcsmm0jrprpb2" }}
+ *
+ * Broken down:
+ * Identity LimitedAccessFeature {{ L"PRODUCTNAME_PUBLISHERID" }}
+ *
+ * That is injected into our build in create_rc.py and is necessary
+ * to unlock the taskbar pinning feature / APIs from an unpackaged
+ * build.
+ *
+ * In the above, the token is generated from the publisher id (pcsmm0jrprpb2)
+ * and the product name (MozillaFirefox)
+ *
+ * All tokens listed here were provided to us by Microsoft.
+ *
+ * Below and in create_rc.py, we used this set:
+ *
+ * Token: "kRFiWpEK5uS6PMJZKmR7MQ=="
+ * Product Name: "MozillaFirefox"
+ * Publisher ID: "pcsmm0jrprpb2"
+ *
+ * Microsoft also provided these other tokens, which will will
+ * work if accompanied by the matching changes to create_rc.py:
+
+ * -----
+ * Token: "RGEhsYgKhmPLKyzkEHnMhQ=="
+ * Product Name: "FirefoxBeta"
+ * Publisher ID: "pcsmm0jrprpb2"
+ *
+ * -----
+ *
+ * Token: "qbVzns/9kT+t15YbIwT4Jw=="
+ * Product Name: "FirefoxNightly"
+ * Publisher ID: "pcsmm0jrprpb2"
+ *
+ * To use those instead, you have to ensure that the LimitedAccessFeature
+ * generated in create_rc.py has the product name and publisher id
+ * matching the token used in this file.
+ *
+ * For non-packaged (non-MSIX) builds, any of the above sets will work.
+ * Just make sure the right (ProductName_PublisherID) value is in the
+ * generated resource data for the executable, and the matching
+* (Token) and attestation string
+ *
+ * To get MSIX/packaged builds to work, the product name and publisher in
+ * the final manifest (searchfox.org/mozilla-central/search?q=APPX_PUBLISHER)
+ * should match the token in this file. For that case, the identity value
+ * in the resources does not matter.
+ *
+ * See here for Microsoft examples:
+https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/TaskbarManager/CppUnpackagedDesktopTaskbarPin
+ */
+
+struct LimitedAccessFeatureInfo {
+ const char* debugName;
+ const WCHAR* feature;
+ const WCHAR* token;
+ const WCHAR* attestation;
+};
+
+static LimitedAccessFeatureInfo limitedAccessFeatureInfo[] = {
+ {// Win11LimitedAccessFeatureType::Taskbar
+ "Win11LimitedAccessFeatureType::Taskbar",
+ L"com.microsoft.windows.taskbar.pin", L"kRFiWpEK5uS6PMJZKmR7MQ==",
+ L"pcsmm0jrprpb2 has registered their use of "
+ L"com.microsoft.windows.taskbar.pin with Microsoft and agrees to the "
+ L"terms "
+ L"of use."}};
+
+static_assert(mozilla::ArrayLength(limitedAccessFeatureInfo) ==
+ kWin11LimitedAccessFeatureTypeCount);
+
+/**
+ Implementation of the Win11LimitedAccessFeaturesInterface.
+ */
+class Win11LimitedAccessFeatures : public Win11LimitedAccessFeaturesInterface {
+ public:
+ using AtomicState = Atomic<int, SequentiallyConsistent>;
+
+ Result<bool, HRESULT> Unlock(Win11LimitedAccessFeatureType feature) override;
+
+ private:
+ AtomicState& GetState(Win11LimitedAccessFeatureType feature);
+ Result<bool, HRESULT> UnlockImplementation(
+ Win11LimitedAccessFeatureType feature);
+
+ /**
+ * Store the state as an atomic so that it can be safely accessed from
+ * different threads.
+ */
+ static AtomicState mTaskbarState;
+ static AtomicState mDefaultState;
+
+ enum State {
+ Uninitialized,
+ Locked,
+ Unlocked,
+ };
+};
+
+Win11LimitedAccessFeatures::AtomicState
+ Win11LimitedAccessFeatures::mTaskbarState(
+ Win11LimitedAccessFeatures::Uninitialized);
+Win11LimitedAccessFeatures::AtomicState
+ Win11LimitedAccessFeatures::mDefaultState(
+ Win11LimitedAccessFeatures::Uninitialized);
+
+RefPtr<Win11LimitedAccessFeaturesInterface>
+CreateWin11LimitedAccessFeaturesInterface() {
+ RefPtr<Win11LimitedAccessFeaturesInterface> result(
+ new Win11LimitedAccessFeatures());
+ return result;
+}
+
+Result<bool, HRESULT> Win11LimitedAccessFeatures::Unlock(
+ Win11LimitedAccessFeatureType feature) {
+ AtomicState& atomicState = GetState(feature);
+
+ const auto& lafInfo = limitedAccessFeatureInfo[static_cast<int>(feature)];
+
+ LAF_LOG(
+ LogLevel::Debug, "Limited Access Feature Info for %s. Feature %S, %S, %S",
+ lafInfo.debugName, lafInfo.feature, lafInfo.token, lafInfo.attestation);
+
+ int state = atomicState;
+ if (state != Uninitialized) {
+ LAF_LOG(LogLevel::Debug, "%s already initialized! State = %s",
+ lafInfo.debugName, (state == Unlocked) ? "true" : "false");
+ return (state == Unlocked);
+ }
+
+ // If multiple threads read the state at the same time, and it's unitialized,
+ // both threads will unlock the feature. This situation is unlikely, but even
+ // if it happens, it's not a problem.
+
+ auto result = UnlockImplementation(feature);
+
+ int newState = Locked;
+ if (!result.isErr() && result.unwrap()) {
+ newState = Unlocked;
+ }
+
+ atomicState = newState;
+
+ return result;
+}
+
+Win11LimitedAccessFeatures::AtomicState& Win11LimitedAccessFeatures::GetState(
+ Win11LimitedAccessFeatureType feature) {
+ switch (feature) {
+ case Win11LimitedAccessFeatureType::Taskbar:
+ return mTaskbarState;
+
+ default:
+ LAF_LOG(LogLevel::Debug, "Missing feature type for %d",
+ static_cast<int>(feature));
+ MOZ_ASSERT(false,
+ "Unhandled feature type! Add a new atomic state variable, add "
+ "that entry to the switch statement above, and add the proper "
+ "entries for the feature and the token.");
+ return mDefaultState;
+ }
+}
+
+Result<bool, HRESULT> Win11LimitedAccessFeatures::UnlockImplementation(
+ Win11LimitedAccessFeatureType feature) {
+ ComPtr<ILimitedAccessFeaturesStatics> limitedAccessFeatures;
+ ComPtr<ILimitedAccessFeatureRequestResult> limitedAccessFeaturesResult;
+
+ const auto& lafInfo = limitedAccessFeatureInfo[static_cast<int>(feature)];
+
+ HRESULT hr = RoGetActivationFactory(
+ HStringReference(
+ RuntimeClass_Windows_ApplicationModel_LimitedAccessFeatures)
+ .Get(),
+ IID_ILimitedAccessFeaturesStatics, &limitedAccessFeatures);
+
+ if (!SUCCEEDED(hr)) {
+ LAF_LOG(LogLevel::Debug, "%s activation error. HRESULT = 0x%lx",
+ lafInfo.debugName, hr);
+ return Err(hr);
+ }
+
+ hr = limitedAccessFeatures->TryUnlockFeature(
+ HStringReference(lafInfo.feature).Get(),
+ HStringReference(lafInfo.token).Get(),
+ HStringReference(lafInfo.attestation).Get(),
+ &limitedAccessFeaturesResult);
+ if (!SUCCEEDED(hr)) {
+ LAF_LOG(LogLevel::Debug, "%s unlock error. HRESULT = 0x%lx",
+ lafInfo.debugName, hr);
+ return Err(hr);
+ }
+
+ LimitedAccessFeatureStatus status;
+ hr = limitedAccessFeaturesResult->get_Status(&status);
+ if (!SUCCEEDED(hr)) {
+ LAF_LOG(LogLevel::Debug, "%s get status error. HRESULT = 0x%lx",
+ lafInfo.debugName, hr);
+ return Err(hr);
+ }
+
+ int state = Unlocked;
+ if ((status != LimitedAccessFeatureStatus_Available) &&
+ (status != LimitedAccessFeatureStatus_AvailableWithoutToken)) {
+ LAF_LOG(LogLevel::Debug, "%s not available. HRESULT = 0x%lx",
+ lafInfo.debugName, hr);
+ state = Locked;
+ }
+
+ return (state == Unlocked);
+}
+
+#else // MINGW32 implementation
+
+RefPtr<Win11LimitedAccessFeaturesInterface>
+CreateWin11LimitedAccessFeaturesInterface() {
+ RefPtr<Win11LimitedAccessFeaturesInterface> result;
+ return result;
+}
+
+#endif
diff --git a/browser/components/shell/Windows11LimitedAccessFeatures.h b/browser/components/shell/Windows11LimitedAccessFeatures.h
new file mode 100644
index 0000000000..8e1ae5db7a
--- /dev/null
+++ b/browser/components/shell/Windows11LimitedAccessFeatures.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+#ifndef SHELL_WINDOWS11LIMITEDACCESSFEATURES_H__
+#define SHELL_WINDOWS11LIMITEDACCESSFEATURES_H__
+
+#include "nsISupportsImpl.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultVariant.h"
+#include <winerror.h>
+#include <windows.h> // for HRESULT
+#include "mozilla/DefineEnum.h"
+#include <winerror.h>
+
+MOZ_DEFINE_ENUM_CLASS(Win11LimitedAccessFeatureType, (Taskbar));
+
+/**
+ * Class to manage unlocking limited access features on Windows 11.
+ * Unless stubbing for testing purposes, create objects of this
+ * class with CreateWin11LimitedAccessFeaturesInterface.
+ *
+ * Windows 11 requires certain features to be unlocked in order to work
+ * (for instance, the Win11 Taskbar pinning APIs). Call Unlock()
+ * to unlock them. Generally, results will be cached in atomic variables
+ * and future calls to Unlock will be as long as it takes
+ * to fetch an atomic variable.
+ */
+class Win11LimitedAccessFeaturesInterface {
+ public:
+ /**
+ * Unlocks the limited access features, if possible.
+ *
+ * Returns an error code on error, true on successful unlock,
+ * false on unlock failed (but with no error).
+ */
+ virtual mozilla::Result<bool, HRESULT> Unlock(
+ Win11LimitedAccessFeatureType feature) = 0;
+
+ /**
+ * Reference counting and cycle collection.
+ */
+ NS_INLINE_DECL_REFCOUNTING(Win11LimitedAccessFeaturesInterface)
+
+ protected:
+ virtual ~Win11LimitedAccessFeaturesInterface() {}
+};
+
+RefPtr<Win11LimitedAccessFeaturesInterface>
+CreateWin11LimitedAccessFeaturesInterface();
+
+#endif // SHELL_WINDOWS11LIMITEDACCESSFEATURES_H__
diff --git a/browser/components/shell/Windows11TaskbarPinning.cpp b/browser/components/shell/Windows11TaskbarPinning.cpp
new file mode 100644
index 0000000000..350a58d59a
--- /dev/null
+++ b/browser/components/shell/Windows11TaskbarPinning.cpp
@@ -0,0 +1,344 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+#include "Windows11TaskbarPinning.h"
+#include "Windows11LimitedAccessFeatures.h"
+
+#include "nsWindowsHelpers.h"
+#include "MainThreadUtils.h"
+#include "nsThreadUtils.h"
+#include <strsafe.h>
+
+#include "mozilla/Result.h"
+#include "mozilla/ResultVariant.h"
+
+#include "mozilla/Logging.h"
+
+static mozilla::LazyLogModule sLog("Windows11TaskbarPinning");
+
+#define TASKBAR_PINNING_LOG(level, msg, ...) \
+ MOZ_LOG(sLog, level, (msg, ##__VA_ARGS__))
+
+#ifndef __MINGW32__ // WinRT headers not yet supported by MinGW
+
+# include <wrl.h>
+
+# include <inspectable.h>
+# include <roapi.h>
+# include <windows.services.store.h>
+# include <windows.foundation.h>
+# include <windows.ui.shell.h>
+
+using namespace mozilla;
+
+/**
+ * The Win32 SetEvent and WaitForSingleObject functions take HANDLE parameters
+ * which are typedefs of void*. When using nsAutoHandle, that means if you
+ * forget to call .get() first, everything still compiles and then doesn't work
+ * at runtime. For instance, calling SetEvent(mEvent) below would compile but
+ * not work at runtime and the waits would block forever.
+ * To ensure this isn't an issue, we wrap the event in a custom class here
+ * with the simple methods that we want on an event.
+ */
+class EventWrapper {
+ public:
+ EventWrapper() : mEvent(CreateEventW(nullptr, true, false, nullptr)) {}
+
+ void Set() { SetEvent(mEvent.get()); }
+
+ void Reset() { ResetEvent(mEvent.get()); }
+
+ void Wait() { WaitForSingleObject(mEvent.get(), INFINITE); }
+
+ private:
+ nsAutoHandle mEvent;
+};
+
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+using namespace ABI::Windows;
+using namespace ABI::Windows::UI::Shell;
+using namespace ABI::Windows::Foundation;
+using namespace ABI::Windows::ApplicationModel;
+
+static Result<ComPtr<ITaskbarManager>, HRESULT> InitializeTaskbar() {
+ ComPtr<IInspectable> taskbarStaticsInspectable;
+
+ TASKBAR_PINNING_LOG(LogLevel::Debug, "Initializing taskbar");
+
+ HRESULT hr = RoGetActivationFactory(
+ HStringReference(RuntimeClass_Windows_UI_Shell_TaskbarManager).Get(),
+ IID_ITaskbarManagerStatics, &taskbarStaticsInspectable);
+ if (FAILED(hr)) {
+ TASKBAR_PINNING_LOG(LogLevel::Debug,
+ "Taskbar: Failed to activate. HRESULT = 0x%lx", hr);
+ return Err(hr);
+ }
+
+ ComPtr<ITaskbarManagerStatics> taskbarStatics;
+
+ hr = taskbarStaticsInspectable.As(&taskbarStatics);
+ if (FAILED(hr)) {
+ TASKBAR_PINNING_LOG(LogLevel::Debug, "Failed statistics. HRESULT = 0x%lx",
+ hr);
+ return Err(hr);
+ }
+
+ ComPtr<ITaskbarManager> taskbarManager;
+
+ hr = taskbarStatics->GetDefault(&taskbarManager);
+ if (FAILED(hr)) {
+ TASKBAR_PINNING_LOG(LogLevel::Debug,
+ "Error getting TaskbarManager. HRESULT = 0x%lx", hr);
+ return Err(hr);
+ }
+
+ TASKBAR_PINNING_LOG(LogLevel::Debug,
+ "TaskbarManager retrieved successfully!");
+ return taskbarManager;
+}
+
+Win11PinToTaskBarResult PinCurrentAppToTaskbarWin11(
+ bool aCheckOnly, const nsAString& aAppUserModelId,
+ nsAutoString aShortcutPath) {
+ MOZ_DIAGNOSTIC_ASSERT(!NS_IsMainThread(),
+ "PinCurrentAppToTaskbarWin11 should be called off main "
+ "thread only. It blocks, waiting on things to execute "
+ "asynchronously on the main thread.");
+
+ {
+ RefPtr<Win11LimitedAccessFeaturesInterface> limitedAccessFeatures =
+ CreateWin11LimitedAccessFeaturesInterface();
+ auto result =
+ limitedAccessFeatures->Unlock(Win11LimitedAccessFeatureType::Taskbar);
+ if (result.isErr()) {
+ auto hr = result.unwrapErr();
+ TASKBAR_PINNING_LOG(LogLevel::Debug,
+ "Taskbar unlock: Error. HRESULT = 0x%lx", hr);
+ return {hr, Win11PinToTaskBarResultStatus::NotSupported};
+ }
+
+ if (result.unwrap() == false) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar unlock: failed. Not supported on this version of Windows.");
+ return {S_OK, Win11PinToTaskBarResultStatus::NotSupported};
+ }
+ }
+
+ HRESULT hr;
+ Win11PinToTaskBarResultStatus resultStatus =
+ Win11PinToTaskBarResultStatus::NotSupported;
+
+ EventWrapper event;
+
+ // Everything related to the taskbar and pinning must be done on the main /
+ // user interface thread or Windows will cause them to fail.
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "PinCurrentAppToTaskbarWin11", [&event, &hr, &resultStatus, aCheckOnly] {
+ auto CompletedOperations =
+ [&event, &resultStatus](Win11PinToTaskBarResultStatus status) {
+ resultStatus = status;
+ event.Set();
+ };
+
+ auto result = InitializeTaskbar();
+ if (result.isErr()) {
+ hr = result.unwrapErr();
+ return CompletedOperations(
+ Win11PinToTaskBarResultStatus::NotSupported);
+ }
+
+ ComPtr<ITaskbarManager> taskbar = result.unwrap();
+ boolean supported;
+ hr = taskbar->get_IsSupported(&supported);
+ if (FAILED(hr) || !supported) {
+ if (FAILED(hr)) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: error checking if supported. HRESULT = 0x%lx", hr);
+ } else {
+ TASKBAR_PINNING_LOG(LogLevel::Debug, "Taskbar: not supported.");
+ }
+ return CompletedOperations(
+ Win11PinToTaskBarResultStatus::NotSupported);
+ }
+
+ if (aCheckOnly) {
+ TASKBAR_PINNING_LOG(LogLevel::Debug, "Taskbar: check succeeded.");
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Success);
+ }
+
+ boolean isAllowed = false;
+ hr = taskbar->get_IsPinningAllowed(&isAllowed);
+ if (FAILED(hr) || !isAllowed) {
+ if (FAILED(hr)) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: error checking if pinning is allowed. HRESULT = "
+ "0x%lx",
+ hr);
+ } else {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: is pinning allowed error or isn't allowed right now. "
+ "It's not clear when it will be allowed. Possibly after a "
+ "reboot.");
+ }
+ return CompletedOperations(
+ Win11PinToTaskBarResultStatus::NotCurrentlyAllowed);
+ }
+
+ ComPtr<IAsyncOperation<bool>> isPinnedOperation = nullptr;
+ hr = taskbar->IsCurrentAppPinnedAsync(&isPinnedOperation);
+ if (FAILED(hr)) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: is current app pinned operation failed. HRESULT = "
+ "0x%lx",
+ hr);
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
+ }
+
+ // Copy the taskbar; don't use it as a reference.
+ // With the async calls, it's not guaranteed to still be valid
+ // if sent as a reference.
+ // resultStatus and event are not defined on the main thread and will
+ // be alive until the async functions complete, so they can be used as
+ // references.
+ auto isPinnedCallback = Callback<IAsyncOperationCompletedHandler<
+ bool>>([taskbar, &event, &resultStatus, &hr](
+ IAsyncOperation<bool>* asyncInfo,
+ AsyncStatus status) mutable -> HRESULT {
+ auto CompletedOperations =
+ [&event,
+ &resultStatus](Win11PinToTaskBarResultStatus status) -> HRESULT {
+ resultStatus = status;
+ event.Set();
+ return S_OK;
+ };
+
+ bool asyncOpSucceeded = status == AsyncStatus::Completed;
+ if (!asyncOpSucceeded) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: is pinned operation failed to complete.");
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
+ }
+
+ unsigned char isCurrentAppPinned = false;
+ hr = asyncInfo->GetResults(&isCurrentAppPinned);
+ if (FAILED(hr)) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: is current app pinned check failed. HRESULT = 0x%lx",
+ hr);
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
+ }
+
+ if (isCurrentAppPinned) {
+ TASKBAR_PINNING_LOG(LogLevel::Debug,
+ "Taskbar: current app is already pinned.");
+ return CompletedOperations(
+ Win11PinToTaskBarResultStatus::AlreadyPinned);
+ }
+
+ ComPtr<IAsyncOperation<bool>> requestPinOperation = nullptr;
+ hr = taskbar->RequestPinCurrentAppAsync(&requestPinOperation);
+ if (FAILED(hr)) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: request pin current app operation creation failed. "
+ "HRESULT = 0x%lx",
+ hr);
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
+ }
+
+ auto pinAppCallback = Callback<IAsyncOperationCompletedHandler<
+ bool>>([CompletedOperations, &hr](
+ IAsyncOperation<bool>* asyncInfo,
+ AsyncStatus status) -> HRESULT {
+ bool asyncOpSucceeded = status == AsyncStatus::Completed;
+ if (!asyncOpSucceeded) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: request pin current app operation did not "
+ "complete.");
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
+ }
+
+ unsigned char successfullyPinned = 0;
+ hr = asyncInfo->GetResults(&successfullyPinned);
+ if (FAILED(hr) || !successfullyPinned) {
+ if (FAILED(hr)) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: request pin current app operation failed to pin "
+ "due to error. HRESULT = 0x%lx",
+ hr);
+ } else {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: request pin current app operation failed to pin");
+ }
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
+ }
+
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: request pin current app operation succeeded");
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Success);
+ });
+
+ HRESULT pinOperationHR =
+ requestPinOperation->put_Completed(pinAppCallback.Get());
+ if (FAILED(pinOperationHR)) {
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: request pin operation failed when setting completion "
+ "callback. HRESULT = 0x%lx",
+ hr);
+ hr = pinOperationHR;
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
+ }
+
+ // DO NOT SET event HERE. It will be set in the pin operation
+ // callback As in, operations are not completed, so don't call
+ // CompletedOperations
+ return S_OK;
+ });
+
+ HRESULT isPinnedOperationHR =
+ isPinnedOperation->put_Completed(isPinnedCallback.Get());
+ if (FAILED(isPinnedOperationHR)) {
+ hr = isPinnedOperationHR;
+ TASKBAR_PINNING_LOG(
+ LogLevel::Debug,
+ "Taskbar: is pinned operation failed when setting completion "
+ "callback. HRESULT = 0x%lx",
+ hr);
+ return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
+ }
+
+ // DO NOT SET event HERE. It will be set in the is pin operation
+ // callback As in, operations are not completed, so don't call
+ // CompletedOperations
+ }));
+
+ // block until the pinning is completed on the main thread
+ event.Wait();
+
+ return {hr, resultStatus};
+}
+
+#else // MINGW32 implementation below
+
+Win11PinToTaskBarResult PinCurrentAppToTaskbarWin11(
+ bool aCheckOnly, const nsAString& aAppUserModelId,
+ nsAutoString aShortcutPath) {
+ return {S_OK, Win11PinToTaskBarResultStatus::NotSupported};
+}
+
+#endif // #ifndef __MINGW32__ // WinRT headers not yet supported by MinGW
diff --git a/browser/components/shell/Windows11TaskbarPinning.h b/browser/components/shell/Windows11TaskbarPinning.h
new file mode 100644
index 0000000000..c45fd66a5b
--- /dev/null
+++ b/browser/components/shell/Windows11TaskbarPinning.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */
+
+/**
+ * This file exists to keep the Windows 11 Taskbar Pinning API
+ * related code as self-contained as possible.
+ */
+
+#ifndef SHELL_WINDOWS11TASKBARPINNING_H__
+#define SHELL_WINDOWS11TASKBARPINNING_H__
+
+#include "nsString.h"
+#include <wrl.h>
+#include <windows.h> // for HRESULT
+
+enum class Win11PinToTaskBarResultStatus {
+ Failed,
+ NotCurrentlyAllowed,
+ AlreadyPinned,
+ Success,
+ NotSupported,
+};
+
+struct Win11PinToTaskBarResult {
+ HRESULT errorCode;
+ Win11PinToTaskBarResultStatus result;
+};
+
+Win11PinToTaskBarResult PinCurrentAppToTaskbarWin11(
+ bool aCheckOnly, const nsAString& aAppUserModelId,
+ nsAutoString aShortcutPath);
+
+#endif // SHELL_WINDOWS11TASKBARPINNING_H__
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/moz.build b/browser/components/shell/moz.build
index fe70623907..82e5afade7 100644
--- a/browser/components/shell/moz.build
+++ b/browser/components/shell/moz.build
@@ -50,6 +50,8 @@ elif CONFIG["OS_ARCH"] == "WINNT":
]
SOURCES += [
"nsWindowsShellService.cpp",
+ "Windows11LimitedAccessFeatures.cpp",
+ "Windows11TaskbarPinning.cpp",
"WindowsDefaultBrowser.cpp",
"WindowsUserChoice.cpp",
]
@@ -82,6 +84,7 @@ for var in (
):
DEFINES[var] = '"%s"' % CONFIG[var]
+
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
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/nsWindowsShellService.cpp b/browser/components/shell/nsWindowsShellService.cpp
index 86c7694bf8..f7bca95b09 100644
--- a/browser/components/shell/nsWindowsShellService.cpp
+++ b/browser/components/shell/nsWindowsShellService.cpp
@@ -39,6 +39,7 @@
#include "nsIXULAppInfo.h"
#include "nsINIParser.h"
#include "nsNativeAppSupportWin.h"
+#include "Windows11TaskbarPinning.h"
#include <windows.h>
#include <shellapi.h>
@@ -1626,7 +1627,7 @@ nsWindowsShellService::GetTaskbarTabPins(nsTArray<nsString>& aShortcutPaths) {
static nsresult PinCurrentAppToTaskbarWin10(bool aCheckOnly,
const nsAString& aAppUserModelId,
- nsAutoString aShortcutPath) {
+ const nsAString& aShortcutPath) {
// The behavior here is identical if we're only checking or if we try to pin
// but the app is already pinned so we update the variable accordingly.
if (!aCheckOnly) {
@@ -1695,6 +1696,28 @@ static nsresult PinCurrentAppToTaskbarImpl(
}
}
+ auto pinWithWin11TaskbarAPIResults =
+ PinCurrentAppToTaskbarWin11(aCheckOnly, aAppUserModelId, shortcutPath);
+ switch (pinWithWin11TaskbarAPIResults.result) {
+ case Win11PinToTaskBarResultStatus::NotSupported:
+ // Fall through to the win 10 mechanism
+ break;
+
+ case Win11PinToTaskBarResultStatus::Success:
+ case Win11PinToTaskBarResultStatus::AlreadyPinned:
+ return NS_OK;
+
+ case Win11PinToTaskBarResultStatus::NotCurrentlyAllowed:
+ case Win11PinToTaskBarResultStatus::Failed:
+ // return NS_ERROR_FAILURE;
+
+ // Fall through to the old mechanism for now
+ // In future, we should be sending telemetry for when
+ // an error occurs or for when pinning is not allowed
+ // with the Win 11 APIs.
+ break;
+ }
+
return PinCurrentAppToTaskbarWin10(aCheckOnly, aAppUserModelId, shortcutPath);
}
@@ -1720,7 +1743,7 @@ static nsresult PinCurrentAppToTaskbarAsyncImpl(bool aCheckOnly,
}
nsAutoString aumid;
- if (NS_WARN_IF(!mozilla::widget::WinTaskbar::GenerateAppUserModelID(
+ if (NS_WARN_IF(!mozilla::widget::WinTaskbar::GetAppUserModelID(
aumid, aPrivateBrowsing))) {
return NS_ERROR_FAILURE;
}
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/content/shopping-message-bar.css b/browser/components/shopping/content/shopping-message-bar.css
index f88b18be45..fe3320310f 100644
--- a/browser/components/shopping/content/shopping-message-bar.css
+++ b/browser/components/shopping/content/shopping-message-bar.css
@@ -34,45 +34,37 @@ button {
margin-inline-start: 0;
}
-message-bar::part(container) {
- align-items: start;
- padding: 0.5rem 0.75rem;
+.shopping-message-bar {
+ display: flex;
+ align-items: center;
+ padding-block: 0.5rem;
gap: 0.75rem;
-}
-message-bar::part(icon) {
- padding: 0;
-}
+ &.analysis-in-progress {
+ align-items: start;
+ }
-:host([type=analysis-in-progress]) message-bar::part(icon),
-:host([type=reanalysis-in-progress]) message-bar::part(icon) {
- border: 1px solid var(--icon-color);
- border-radius: 50%;
+ .icon {
+ --message-bar-icon-url: url("chrome://global/skin/icons/info-filled.svg");
+ width: var(--icon-size-default);
+ height: var(--icon-size-default);
+ flex-shrink: 0;
+ appearance: none;
+ -moz-context-properties: fill, stroke;
+ fill: currentColor;
+ stroke: currentColor;
+ color: var(--icon-color);
+ background-image: var(--message-bar-icon-url);
+ }
}
-:host([type=analysis-in-progress]) message-bar::part(icon)::after,
-:host([type=reanalysis-in-progress]) message-bar::part(icon)::after {
+:host([type=analysis-in-progress]) .icon,
+:host([type=reanalysis-in-progress]) .icon {
--message-bar-icon-url: conic-gradient(var(--icon-color-information) var(--analysis-progress-pcent, 0%), transparent var(--analysis-progress-pcent, 0%));
+ border: 1px solid var(--icon-color);
border-radius: 50%;
margin-block: 1px 0;
margin-inline: 1px 0;
- width: calc(var(--icon-size) - 2px);
- height: calc(var(--icon-size) - 2px);
-}
-
-:host([type=reanalysis-in-progress]) message-bar::part(container),
-:host([type=stale]) message-bar::part(container) {
- align-items: center;
- background-color: transparent;
- padding: 0;
-}
-
-:host([type=thank-you-for-feedback]) message-bar::part(icon) {
- --message-bar-icon-url: url("chrome://global/skin/icons/check-filled.svg");
-}
-
-:host([type=thank-you-for-feedback]) message-bar::part(container) {
- text-align: start;
- align-items: center;
- gap: 12px;
+ width: calc(var(--icon-size-default) - 2px);
+ height: calc(var(--icon-size-default) - 2px);
}
diff --git a/browser/components/shopping/content/shopping-message-bar.mjs b/browser/components/shopping/content/shopping-message-bar.mjs
index d6ec9c0888..c8d6510d5e 100644
--- a/browser/components/shopping/content/shopping-message-bar.mjs
+++ b/browser/components/shopping/content/shopping-message-bar.mjs
@@ -95,7 +95,8 @@ class ShoppingMessageBar extends MozLitElement {
}
staleWarningTemplate() {
- return html`<message-bar>
+ return html`<div class="shopping-message-bar">
+ <span class="icon"></span>
<article id="message-bar-container" aria-labelledby="header">
<span
data-l10n-id="shopping-message-bar-warning-stale-analysis-message-2"
@@ -107,7 +108,7 @@ class ShoppingMessageBar extends MozLitElement {
@click=${this.onClickAnalysisButton}
></button>
</article>
- </message-bar>`;
+ </div>`;
}
genericErrorTemplate() {
@@ -163,11 +164,13 @@ class ShoppingMessageBar extends MozLitElement {
}
analysisInProgressTemplate() {
- return html`<message-bar
+ return html`<div
+ class="shopping-message-bar analysis-in-progress"
style=${styleMap({
"--analysis-progress-pcent": `${this.progress}%`,
})}
>
+ <span class="icon"></span>
<article
id="message-bar-container"
aria-labelledby="header"
@@ -184,15 +187,18 @@ class ShoppingMessageBar extends MozLitElement {
data-l10n-id="shopping-message-bar-analysis-in-progress-message2"
></span>
</article>
- </message-bar>`;
+ </div>`;
}
reanalysisInProgressTemplate() {
- return html`<message-bar
+ return html`<div
+ class="shopping-message-bar"
+ id="reanalysis-in-progress-message"
style=${styleMap({
"--analysis-progress-pcent": `${this.progress}%`,
})}
>
+ <span class="icon"></span>
<article
id="message-bar-container"
aria-labelledby="header"
@@ -206,7 +212,7 @@ class ShoppingMessageBar extends MozLitElement {
})}"
></span>
</article>
- </message-bar>`;
+ </div>`;
}
pageNotSupportedTemplate() {
diff --git a/browser/components/shopping/content/shopping.html b/browser/components/shopping/content/shopping.html
index 1c5d627869..8c5da71a96 100644
--- a/browser/components/shopping/content/shopping.html
+++ b/browser/components/shopping/content/shopping.html
@@ -30,7 +30,6 @@
href="chrome://browser/content/shopping/shopping-page.css"
/>
- <script src="chrome://global/content/elements/message-bar.js"></script>
<script
type="module"
src="chrome://browser/content/shopping/onboarding.mjs"
diff --git a/browser/components/shopping/metrics.yaml b/browser/components/shopping/metrics.yaml
index b1869e859a..61be0c2985 100644
--- a/browser/components/shopping/metrics.yaml
+++ b/browser/components/shopping/metrics.yaml
@@ -29,6 +29,8 @@ shopping.settings:
send_in_pings:
- metrics
telemetry_mirror: SHOPPING_NIMBUS_DISABLED
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
component_opted_out:
type: boolean
@@ -49,6 +51,8 @@ shopping.settings:
send_in_pings:
- metrics
telemetry_mirror: SHOPPING_COMPONENT_OPTED_OUT
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
has_onboarded:
type: boolean
@@ -70,6 +74,8 @@ shopping.settings:
send_in_pings:
- metrics
telemetry_mirror: SHOPPING_HAS_ONBOARDED
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
disabled_ads:
type: boolean
@@ -90,6 +96,8 @@ shopping.settings:
send_in_pings:
- metrics
telemetry_mirror: SHOPPING_DISABLED_ADS
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
auto_open_user_disabled:
type: boolean
@@ -110,6 +118,8 @@ shopping.settings:
send_in_pings:
- metrics
telemetry_mirror: SHOPPING_AUTO_OPEN_USER_DISABLED
+ no_lint:
+ - GIFFT_NON_PING_LIFETIME
shopping:
surface_displayed:
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_inprogress_analysis.js b/browser/components/shopping/tests/browser/browser_inprogress_analysis.js
index d2d1ddeb8c..67d0b54be2 100644
--- a/browser/components/shopping/tests/browser/browser_inprogress_analysis.js
+++ b/browser/components/shopping/tests/browser/browser_inprogress_analysis.js
@@ -127,8 +127,9 @@ add_task(async function test_in_progress_analysis_stale() {
"shopping-message-bar should have progress"
);
- let messageBarEl =
- shoppingMessageBarEl?.shadowRoot.querySelector("message-bar");
+ let messageBarEl = shoppingMessageBarEl?.shadowRoot.getElementById(
+ "reanalysis-in-progress-message"
+ );
is(
messageBarEl?.getAttribute("style"),
"--analysis-progress-pcent: 50%;",
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..6cbac7c082 100644
--- a/browser/base/content/browser-sidebar.js
+++ b/browser/components/sidebar/browser-sidebar.js
@@ -3,53 +3,127 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
- * SidebarUI controls showing and hiding the browser sidebar.
+ * SidebarController handles logic such as toggling sidebar panels,
+ * dynamically adding menubar menu items for the View -> Sidebar menu,
+ * and provides APIs for sidebar extensions, etc.
*/
-var SidebarUI = {
+var SidebarController = {
+ makeSidebar({ elementId, ...rest }) {
+ return {
+ get sourceL10nEl() {
+ return document.getElementById(elementId);
+ },
+ get title() {
+ return document.getElementById(elementId).getAttribute("label");
+ },
+ ...rest,
+ };
+ },
+
get sidebars() {
if (this._sidebars) {
return this._sidebars;
}
- function makeSidebar({ elementId, ...rest }) {
- return {
- get sourceL10nEl() {
- return document.getElementById(elementId);
- },
- get title() {
- return document.getElementById(elementId).getAttribute("label");
- },
- ...rest,
- };
- }
-
- return (this._sidebars = new Map([
- [
- "viewBookmarksSidebar",
- makeSidebar({
- elementId: "sidebar-switcher-bookmarks",
- url: "chrome://browser/content/places/bookmarksSidebar.xhtml",
- menuId: "menu_bookmarksSidebar",
- }),
- ],
+ this._sidebars = new Map([
[
"viewHistorySidebar",
- makeSidebar({
+ this.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",
+ keyId: "key_gotoHistory",
+ menuL10nId: "menu-view-history-button",
+ revampL10nId: "sidebar-menu-history",
+ icon: `url("chrome://browser/content/firefoxview/view-history.svg")`,
}),
],
[
"viewTabsSidebar",
- makeSidebar({
+ this.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",
+ classAttribute: "sync-ui-item",
+ menuL10nId: "menu-view-synced-tabs-sidebar",
+ revampL10nId: "sidebar-menu-synced-tabs",
+ icon: `url("chrome://browser/content/firefoxview/view-syncedtabs.svg")`,
}),
],
- ]));
+ ]);
+
+ if (!this.sidebarRevampEnabled) {
+ this._sidebars.set(
+ "viewBookmarksSidebar",
+ this.makeSidebar({
+ elementId: "sidebar-switcher-bookmarks",
+ url: "chrome://browser/content/places/bookmarksSidebar.xhtml",
+ menuId: "menu_bookmarksSidebar",
+ keyId: "viewBookmarksSidebarKb",
+ menuL10nId: "menu-view-bookmarks",
+ revampL10nId: "sidebar-menu-bookmarks",
+ })
+ );
+ if (this.megalistEnabled) {
+ this._sidebars.set(
+ "viewMegalistSidebar",
+ this.makeSidebar({
+ elementId: "sidebar-switcher-megalist",
+ url: "chrome://global/content/megalist/megalist.html",
+ menuId: "menu_megalistSidebar",
+ menuL10nId: "menu-view-megalist-sidebar",
+ revampL10nId: "sidebar-menu-megalist",
+ })
+ );
+ }
+ } else {
+ this._sidebars.set(
+ "viewCustomizeSidebar",
+ this.makeSidebar({
+ url: "chrome://browser/content/sidebar/sidebar-customize.html",
+ revampL10nId: "sidebar-menu-customize",
+ icon: `url("chrome://browser/skin/preferences/category-general.svg")`,
+ })
+ );
+ }
+
+ return this._sidebars;
+ },
+
+ /**
+ * Returns a map of tools and extensions for use in the sidebar
+ */
+ get toolsAndExtensions() {
+ if (this._toolsAndExtensions) {
+ return this._toolsAndExtensions;
+ }
+
+ this._toolsAndExtensions = new Map();
+ this.getSidebarPanels(["viewHistorySidebar", "viewTabsSidebar"]).forEach(
+ tool => {
+ this._toolsAndExtensions.set(tool.commandID, {
+ view: tool.commandID,
+ icon: tool.icon,
+ l10nId: tool.revampL10nId,
+ disabled: false,
+ });
+ }
+ );
+ this.getExtensions().forEach(extension => {
+ this._toolsAndExtensions.set(extension.commandID, {
+ view: extension.commandID,
+ extensionId: extension.extensionId,
+ icon: extension.icon,
+ tooltiptext: extension.label,
+ disabled: false,
+ });
+ });
+ return this._toolsAndExtensions;
},
// Avoid getting the browser element from init() to avoid triggering the
@@ -98,7 +172,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 +182,27 @@ 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);
- });
+ const menubar = document.getElementById("viewSidebarMenu");
+ for (const [commandID, sidebar] of this.sidebars.entries()) {
+ if (!Object.hasOwn(sidebar, "extensionId")) {
+ // registerExtension() already creates menu items for extensions.
+ const menuitem = this.createMenuItem(commandID, sidebar);
+ menubar.appendChild(menuitem);
+ }
+ }
+
+ if (this.sidebarRevampEnabled) {
+ await import("chrome://browser/content/sidebar/sidebar-main.mjs");
+ document.getElementById("sidebar-main").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,28 +211,30 @@ var SidebarUI = {
this._initDeferred.resolve();
},
+ toggleMegalistItem() {
+ const sideMenuPopupItem = document.getElementById(
+ "sidebar-switcher-megalist"
+ );
+ sideMenuPopupItem.style.display = this.megalistEnabled ? "" : "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 = !this.megalistEnabled;
+ },
+
uninit() {
// If this is the last browser window, persist various values that should be
// remembered for after a restart / reopening a browser window.
let enumerator = Services.wm.getEnumerator("navigator:browser");
if (!enumerator.hasMoreElements()) {
let xulStore = Services.xulStore;
- xulStore.persist(this._box, "sidebarcommand");
-
- if (this._box.hasAttribute("positionend")) {
- xulStore.persist(this._box, "positionend");
- } else {
- xulStore.removeValue(
- document.documentURI,
- "sidebar-box",
- "positionend"
- );
- }
- if (this._box.hasAttribute("checked")) {
- xulStore.persist(this._box, "checked");
- } else {
- xulStore.removeValue(document.documentURI, "sidebar-box", "checked");
- }
xulStore.persist(this._box, "style");
xulStore.persist(this._title, "value");
@@ -159,7 +250,7 @@ var SidebarUI = {
/**
* The handler for Services.obs.addObserver.
- **/
+ */
observe(_subject, topic, _data) {
switch (topic) {
case "intl:app-locales-changed": {
@@ -216,6 +307,7 @@ var SidebarUI = {
/**
* Handles keydown on the the switcherTarget button
+ *
* @param {Event} event
*/
handleKeydown(event) {
@@ -241,6 +333,7 @@ var SidebarUI = {
},
showSwitcherPanel() {
+ this.toggleMegalistItem();
this._switcherPanel.addEventListener(
"popuphiding",
() => {
@@ -292,24 +385,30 @@ var SidebarUI = {
[...browser.children].forEach((node, i) => {
node.style.order = i + 1;
});
+ let sidebarMain = document.querySelector("sidebar-main");
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-main | sidebar-box | splitter | appcontent |
+ // Want to display as: | appcontent | splitter | sidebar-box | sidebar-main
+ // So we just swap box and appcontent ordering and move sidebar-main 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
+ sidebarMain.style.order = parseInt(this._box.style.order) + 1;
// Indicate we've switched ordering to the box
this._box.setAttribute("positionend", true);
+ sidebarMain.setAttribute("positionend", true);
} else {
this._box.removeAttribute("positionend");
+ sidebarMain.removeAttribute("positionend");
}
this.hideSwitcherPanel();
- let content = SidebarUI.browser.contentWindow;
+ let content = SidebarController.browser.contentWindow;
if (content && content.updatePosition) {
content.updatePosition();
}
@@ -317,15 +416,16 @@ 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) {
// If the opener had a sidebar, open the same sidebar in our window.
// The opener can be the hidden window too, if we're coming from the state
// where no windows are open, and the hidden window has no sidebar box.
- let sourceUI = sourceWindow.SidebarUI;
+ let sourceUI = sourceWindow.SidebarController;
if (!sourceUI || !sourceUI._box) {
// no source UI or no _box means we also can't adopt the state.
return false;
@@ -461,7 +561,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 (
@@ -490,17 +590,212 @@ var SidebarUI = {
_loadSidebarExtension(commandID) {
let sidebar = this.sidebars.get(commandID);
- let { extensionId } = sidebar;
- if (extensionId) {
- SidebarUI.browser.contentWindow.loadPanel(
- extensionId,
- sidebar.panel,
- sidebar.browserStyle
- );
+ if (typeof sidebar.onload === "function") {
+ sidebar.onload();
+ }
+ },
+
+ /**
+ * Sets the disabled property for a tool when customizing sidebar options
+ *
+ * @param {string} commandID
+ */
+ toggleTool(commandID) {
+ let toggledTool = this.toolsAndExtensions.get(commandID);
+ toggledTool.disabled = !toggledTool.disabled;
+ if (!toggledTool.disabled) {
+ // If re-enabling tool, remove from the map and add it to the end
+ this.toolsAndExtensions.delete(commandID);
+ this.toolsAndExtensions.set(commandID, toggledTool);
+ }
+ window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
+ },
+
+ addOrUpdateExtension(commandID, extension) {
+ if (this.toolsAndExtensions.has(commandID)) {
+ // Update existing extension
+ let extensionToUpdate = this.toolsAndExtensions.get(commandID);
+ extensionToUpdate.icon = extension.icon;
+ extensionToUpdate.tooltiptext = extension.label;
+ window.dispatchEvent(new CustomEvent("SidebarItemChanged"));
+ } else {
+ // Add new extension
+ this.toolsAndExtensions.set(commandID, {
+ view: commandID,
+ extensionId: extension.extensionId,
+ icon: extension.icon,
+ tooltiptext: extension.label,
+ disabled: false,
+ });
+ window.dispatchEvent(new CustomEvent("SidebarItemAdded"));
+ }
+ },
+
+ /**
+ * Add menu items for a browser extension. Add the extension to the
+ * `sidebars` map.
+ *
+ * @param {string} commandID
+ * @param {object} props
+ */
+ registerExtension(commandID, props) {
+ const sidebar = {
+ title: props.title,
+ url: "chrome://browser/content/webext-panels.xhtml",
+ menuId: props.menuId,
+ switcherMenuId: `sidebarswitcher_menu_${commandID}`,
+ keyId: `ext-key-id-${commandID}`,
+ label: props.title,
+ icon: props.icon,
+ classAttribute: "menuitem-iconic webextension-menuitem",
+ // The following properties are specific to extensions
+ extensionId: props.extensionId,
+ onload: props.onload,
+ };
+ this.sidebars.set(commandID, sidebar);
+
+ // Insert a menuitem for View->Show Sidebars.
+ const menuitem = this.createMenuItem(commandID, sidebar);
+ document.getElementById("viewSidebarMenu").appendChild(menuitem);
+ this.addOrUpdateExtension(commandID, sidebar);
+
+ if (!this.sidebarRevampEnabled) {
+ // Insert a toolbarbutton for the sidebar dropdown selector.
+ let switcherMenuitem = this.createMenuItem(commandID, sidebar);
+ switcherMenuitem.setAttribute("id", sidebar.switcherMenuId);
+ switcherMenuitem.removeAttribute("type");
+
+ let separator = document.getElementById("sidebar-extensions-separator");
+ separator.parentNode.insertBefore(switcherMenuitem, separator);
+ }
+ this._setExtensionAttributes(
+ commandID,
+ { icon: props.icon, label: props.title },
+ sidebar
+ );
+ },
+
+ /**
+ * Create a menu item for the View>Sidebars submenu in the menubar.
+ *
+ * @param {string} commandID
+ * @param {object} sidebar
+ * @returns {Element}
+ */
+ createMenuItem(commandID, sidebar) {
+ const menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("id", sidebar.menuId);
+ menuitem.setAttribute("type", "checkbox");
+ menuitem.addEventListener("command", () => this.toggle(commandID));
+ if (sidebar.classAttribute) {
+ menuitem.setAttribute("class", sidebar.classAttribute);
+ }
+ if (sidebar.keyId) {
+ menuitem.setAttribute("key", sidebar.keyId);
+ }
+ if (sidebar.menuL10nId) {
+ menuitem.dataset.l10nId = sidebar.menuL10nId;
+ }
+ return menuitem;
+ },
+
+ /**
+ * Update attributes on all existing menu items for a browser extension.
+ *
+ * @param {string} commandID
+ * @param {object} attributes
+ * @param {string} attributes.icon
+ * @param {string} attributes.label
+ * @param {boolean} needsRefresh
+ */
+ setExtensionAttributes(commandID, attributes, needsRefresh) {
+ const sidebar = this.sidebars.get(commandID);
+ this._setExtensionAttributes(commandID, attributes, sidebar, needsRefresh);
+ this.addOrUpdateExtension(commandID, sidebar);
+ },
+
+ _setExtensionAttributes(
+ commandID,
+ { icon, label },
+ sidebar,
+ needsRefresh = false
+ ) {
+ sidebar.icon = icon;
+ sidebar.label = label;
+
+ const updateAttributes = el => {
+ el.style.setProperty("--webextension-menuitem-image", sidebar.icon);
+ el.setAttribute("label", sidebar.label);
+ };
+
+ updateAttributes(document.getElementById(sidebar.menuId), sidebar);
+ const switcherMenu = document.getElementById(sidebar.switcherMenuId);
+ if (switcherMenu) {
+ updateAttributes(switcherMenu, sidebar);
+ }
+ if (this.initialized && this.currentID === commandID) {
+ // Update the sidebar if this extension is the current sidebar.
+ updateAttributes(this._switcherTarget, sidebar);
+ this.title = label;
+ if (this.isOpen && needsRefresh) {
+ this.show(commandID);
+ }
}
},
/**
+ * Retrieve the list of registered browser extensions.
+ *
+ * @returns {Array}
+ */
+ getExtensions() {
+ const extensions = [];
+ for (const [commandID, sidebar] of this.sidebars.entries()) {
+ if (Object.hasOwn(sidebar, "extensionId")) {
+ extensions.push({ commandID, ...sidebar });
+ }
+ }
+ return extensions;
+ },
+
+ /**
+ * Retrieve the list of sidebar panels
+ *
+ * @param {Array} commandIds
+ * @returns {Array}
+ */
+ getSidebarPanels(commandIds) {
+ const tools = [];
+ for (const commandID of commandIds) {
+ const sidebar = this.sidebars.get(commandID);
+ if (sidebar) {
+ tools.push({ commandID, ...sidebar });
+ }
+ }
+ return tools;
+ },
+
+ /**
+ * Remove a browser extension.
+ *
+ * @param {string} commandID
+ */
+ removeExtension(commandID) {
+ const sidebar = this.sidebars.get(commandID);
+ if (!sidebar) {
+ return;
+ }
+ if (this.currentID === commandID) {
+ this.hide();
+ }
+ document.getElementById(sidebar.menuId)?.remove();
+ document.getElementById(sidebar.switcherMenuId)?.remove();
+ this.sidebars.delete(commandID);
+ this.toolsAndExtensions.delete(commandID);
+ window.dispatchEvent(new CustomEvent("SidebarItemRemoved"));
+ },
+
+ /**
* Show the sidebar.
*
* This wraps the internal method, including a ping to telemetry.
@@ -508,7 +803,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 +832,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 +854,50 @@ 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);
+ let { icon, url, title, sourceL10nEl } = this.sidebars.get(commandID);
+ if (icon) {
+ this._switcherTarget.style.setProperty(
+ "--webextension-menuitem-image",
+ icon
+ );
+ } else {
+ this._switcherTarget.style.removeProperty(
+ "--webextension-menuitem-image"
+ );
+ }
+
+ // 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 +930,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
@@ -666,9 +982,21 @@ var SidebarUI = {
// Add getters related to the position here, since we will want them
// available for both startDelayedLoad and init.
XPCOMUtils.defineLazyPreferenceGetter(
- SidebarUI,
+ SidebarController,
"_positionStart",
- SidebarUI.POSITION_START_PREF,
+ SidebarController.POSITION_START_PREF,
true,
- SidebarUI.setPosition.bind(SidebarUI)
+ SidebarController.setPosition.bind(SidebarController)
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ SidebarController,
+ "sidebarRevampEnabled",
+ "sidebar.revamp",
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ SidebarController,
+ "megalistEnabled",
+ "browser.megalist.enabled",
+ false
);
diff --git a/browser/components/sidebar/jar.mn b/browser/components/sidebar/jar.mn
index c3d7f0cbcf..f9624b2a55 100644
--- a/browser/components/sidebar/jar.mn
+++ b/browser/components/sidebar/jar.mn
@@ -3,3 +3,15 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
+ content/browser/sidebar/browser-sidebar.js
+ content/browser/sidebar/sidebar-customize.css
+ content/browser/sidebar/sidebar-customize.html
+ content/browser/sidebar/sidebar-customize.mjs
+ content/browser/sidebar/sidebar-main.css
+ content/browser/sidebar/sidebar-main.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/moz.build b/browser/components/sidebar/moz.build
index d988c0ff9b..6310d973e0 100644
--- a/browser/components/sidebar/moz.build
+++ b/browser/components/sidebar/moz.build
@@ -5,3 +5,5 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
JAR_MANIFESTS += ["jar.mn"]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
diff --git a/browser/components/sidebar/sidebar-customize.css b/browser/components/sidebar/sidebar-customize.css
new file mode 100644
index 0000000000..d78477e8ba
--- /dev/null
+++ b/browser/components/sidebar/sidebar-customize.css
@@ -0,0 +1,57 @@
+/* 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/. */
+
+.container {
+ padding-inline: var(--space-small);
+ font-size: 15px;
+}
+
+.customize-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ color: currentColor;
+
+ .customize-close-button::part(button) {
+ background-image: url("chrome://global/skin/icons/close-12.svg");
+ }
+}
+
+.customize-firefox-tools {
+ .inputs {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-medium);
+ }
+
+ .input-wrapper {
+ display: flex;
+ align-items: center;
+ gap: var(--space-small);
+
+ > input {
+ margin-inline-start: 0;
+ }
+
+ > label {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-small);
+ font-size: 0.9em;
+ }
+
+ .icon {
+ -moz-context-properties: fill;
+ fill: currentColor;
+ background-image: var(--tool-icon);
+ background-size: var(--icon-size-default);
+ width: var(--icon-size-default);
+ height: var(--icon-size-default);
+ background-position: center;
+ background-repeat: no-repeat;
+ }
+ }
+}
diff --git a/browser/components/sidebar/sidebar-customize.html b/browser/components/sidebar/sidebar-customize.html
new file mode 100644
index 0000000000..24ba42c210
--- /dev/null
+++ b/browser/components/sidebar/sidebar-customize.html
@@ -0,0 +1,31 @@
+<!-- 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="sidebar-customize-header"></title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="preview/sidebar.ftl" />
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/sidebar/sidebar.css"
+ />
+ <script
+ type="module"
+ src="chrome://browser/content/sidebar/sidebar-customize.mjs"
+ ></script>
+ <script src="chrome://browser/content/contentTheme.js"></script>
+ </head>
+
+ <body>
+ <sidebar-customize />
+ </body>
+</html>
diff --git a/browser/components/sidebar/sidebar-customize.mjs b/browser/components/sidebar/sidebar-customize.mjs
new file mode 100644
index 0000000000..e4ad5fe5dd
--- /dev/null
+++ b/browser/components/sidebar/sidebar-customize.mjs
@@ -0,0 +1,116 @@
+/* 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, styleMap } from "chrome://global/content/vendor/lit.all.mjs";
+
+import { SidebarPage } from "./sidebar-page.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
+
+const l10nMap = new Map([
+ ["viewHistorySidebar", "sidebar-customize-history"],
+ ["viewTabsSidebar", "sidebar-customize-synced-tabs"],
+]);
+
+export class SidebarCustomize extends SidebarPage {
+ static queries = {
+ toolInputs: { all: ".customize-firefox-tools input" },
+ };
+
+ connectedCallback() {
+ super.connectedCallback();
+ window.addEventListener("SidebarItemChanged", this);
+ }
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ window.removeEventListener("SidebarItemChanged", this);
+ }
+
+ get sidebarLauncher() {
+ return this.getWindow().document.querySelector("sidebar-launcher");
+ }
+
+ getWindow() {
+ return window.browsingContext.embedderWindowGlobal.browsingContext.window;
+ }
+
+ closeCustomizeView(e) {
+ e.preventDefault();
+ let view = e.target.getAttribute("view");
+ this.getWindow().SidebarController.toggle(view);
+ }
+
+ getTools() {
+ const toolsMap = new Map(
+ [...this.getWindow().SidebarController.toolsAndExtensions]
+ // eslint-disable-next-line no-unused-vars
+ .filter(([key, val]) => !val.extensionId)
+ );
+ return toolsMap;
+ }
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "SidebarItemChanged":
+ this.requestUpdate();
+ break;
+ }
+ }
+
+ async onToggleInput(e) {
+ e.preventDefault();
+ this.getWindow().SidebarController.toggleTool(e.target.id);
+ }
+
+ getInputL10nId(view) {
+ return l10nMap.get(view);
+ }
+
+ inputTemplate(tool) {
+ return html`<div class="input-wrapper">
+ <input
+ type="checkbox"
+ id=${tool.view}
+ name=${tool.view}
+ @change=${this.onToggleInput}
+ ?checked=${!tool.disabled}
+ />
+ <label for=${tool.view}
+ ><span class="icon ghost-icon" style=${styleMap({
+ "--tool-icon": tool.icon,
+ })} role="presentation"/></span><span
+ data-l10n-id=${this.getInputL10nId(tool.view)}
+ ></span
+ ></label>
+ </div>`;
+ }
+
+ render() {
+ return html`
+ ${this.stylesheet()}
+ <link rel="stylesheet" href="chrome://browser/content/sidebar/sidebar-customize.css"></link>
+ <div class="container">
+ <div class="customize-header">
+ <h2 data-l10n-id="sidebar-customize-header"></h2>
+ <moz-button
+ class="customize-close-button"
+ @click=${this.closeCustomizeView}
+ view="viewCustomizeSidebar"
+ size="default"
+ type="icon ghost"
+ >
+ </moz-button>
+ </div>
+ <div class="customize-firefox-tools">
+ <h5 data-l10n-id="sidebar-customize-firefox-tools"></h5>
+ <div class="inputs">
+ ${[...this.getTools().values()].map(tool => this.inputTemplate(tool))}
+ </div>
+ </div>
+ </div>
+ `;
+ }
+}
+
+customElements.define("sidebar-customize", SidebarCustomize);
diff --git a/browser/components/sidebar/sidebar-history.html b/browser/components/sidebar/sidebar-history.html
new file mode 100644
index 0000000000..9544aa58d6
--- /dev/null
+++ b/browser/components/sidebar/sidebar-history.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" />
+ <title data-l10n-id="firefoxview-page-title"></title>
+ <link rel="localization" href="branding/brand.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/firefoxview/fxview-search-textbox.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://browser/content/firefoxview/fxview-tab-list.mjs"
+ ></script>
+ <script
+ type="module"
+ src="chrome://global/content/elements/moz-card.mjs"
+ ></script>
+ <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..c381d48eed
--- /dev/null
+++ b/browser/components/sidebar/sidebar-history.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 = {};
+
+import { html, when } from "chrome://global/content/vendor/lit.all.mjs";
+import { navigateToLink } from "chrome://browser/content/firefoxview/helpers.mjs";
+
+import { SidebarPage } from "./sidebar-page.mjs";
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ HistoryController: "resource:///modules/HistoryController.sys.mjs",
+});
+
+const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
+
+export class SidebarHistory extends SidebarPage {
+ static queries = {
+ lists: { all: "fxview-tab-list" },
+ searchTextbox: "fxview-search-textbox",
+ };
+
+ constructor() {
+ super();
+ this._started = false;
+ // Setting maxTabsLength to -1 for no max
+ this.maxTabsLength = -1;
+ }
+
+ controller = new lazy.HistoryController(this, {
+ component: "sidebar",
+ });
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.controller.updateCache();
+ }
+
+ 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.isHistoryEmpty) {
+ return this.#historyCardsTemplate();
+ }
+ return this.#emptyMessageTemplate();
+ }
+
+ #historyCardsTemplate() {
+ return this.controller.historyVisits.map(historyItem => {
+ let dateArg = JSON.stringify({ date: historyItem.items[0].time });
+ return 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>`;
+ });
+ }
+
+ #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>`;
+ }
+
+ onSearchQuery(e) {
+ 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>
+ `;
+ }
+}
+
+customElements.define("sidebar-history", SidebarHistory);
diff --git a/browser/components/sidebar/sidebar-main.css b/browser/components/sidebar/sidebar-main.css
new file mode 100644
index 0000000000..14fac4d773
--- /dev/null
+++ b/browser/components/sidebar/sidebar-main.css
@@ -0,0 +1,29 @@
+/* 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;
+ }
+}
+
+.actions-list {
+ display: flex;
+ flex-direction: column;
+ justify-content: end;
+ gap: var(--space-xsmall);
+}
+
+.icon-button::part(button) {
+ background-image: var(--action-icon);
+}
diff --git a/browser/components/sidebar/sidebar-main.mjs b/browser/components/sidebar/sidebar-main.mjs
new file mode 100644
index 0000000000..c7c65d18e8
--- /dev/null
+++ b/browser/components/sidebar/sidebar-main.mjs
@@ -0,0 +1,163 @@
+/* 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";
+
+/**
+ * Sidebar with expanded and collapsed states that provides entry points
+ * to various sidebar panels and sidebar extensions.
+ */
+export default class SidebarMain extends MozLitElement {
+ static properties = {
+ bottomActions: { type: Array },
+ selectedView: { type: String },
+ sidebarItems: { type: Array },
+ open: { type: Boolean },
+ };
+
+ static queries = {
+ extensionButtons: { all: ".tools-and-extensions > moz-button[extension]" },
+ toolButtons: { all: ".tools-and-extensions > moz-button:not([extension])" },
+ customizeButton: ".bottom-actions > moz-button[view=viewCustomizeSidebar]",
+ };
+
+ constructor() {
+ super();
+ this.bottomActions = [];
+ this.selectedView = window.SidebarController.currentID;
+ this.open = window.SidebarController.isOpen;
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this._sidebarBox = document.getElementById("sidebar-box");
+ this._sidebarBox.addEventListener("sidebar-show", this);
+ this._sidebarBox.addEventListener("sidebar-hide", this);
+
+ window.addEventListener("SidebarItemAdded", this);
+ window.addEventListener("SidebarItemChanged", this);
+ window.addEventListener("SidebarItemRemoved", this);
+
+ this.setCustomize();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this._sidebarBox.removeEventListener("sidebar-show", this);
+ this._sidebarBox.removeEventListener("sidebar-hide", this);
+
+ window.removeEventListener("SidebarItemAdded", this);
+ window.removeEventListener("SidebarItemChanged", this);
+ window.removeEventListener("SidebarItemRemoved", this);
+ }
+
+ 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;
+ }
+
+ getToolsAndExtensions() {
+ return window.SidebarController.toolsAndExtensions;
+ }
+
+ setCustomize() {
+ this.bottomActions.push(
+ ...window.SidebarController.getSidebarPanels([
+ "viewCustomizeSidebar",
+ ]).map(({ commandID, icon, revampL10nId }) => ({
+ l10nId: revampL10nId,
+ icon,
+ view: commandID,
+ }))
+ );
+ }
+
+ handleEvent(e) {
+ switch (e.type) {
+ case "sidebar-show":
+ this.selectedView = e.detail.viewId;
+ this.open = true;
+ break;
+ case "sidebar-hide":
+ this.open = false;
+ break;
+ case "SidebarItemAdded":
+ case "SidebarItemChanged":
+ case "SidebarItemRemoved":
+ this.requestUpdate();
+ break;
+ }
+ }
+
+ showView(e) {
+ let view = e.target.getAttribute("view");
+ window.SidebarController.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 })}
+ ?extension=${action.view?.includes("-sidebar-action")}
+ >
+ </moz-button>`;
+ }
+
+ render() {
+ let toolsAndExtensions = this.getToolsAndExtensions()
+ ? this.getToolsAndExtensions()
+ : new Map();
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/sidebar/sidebar-main.css"
+ />
+ <div class="wrapper">
+ <div class="tools-and-extensions actions-list">
+ ${[...toolsAndExtensions.values()]
+ .filter(toolOrExtension => !toolOrExtension.disabled)
+ .map(action => this.entrypointTemplate(action))}
+ </div>
+ <div class="bottom-actions actions-list">
+ ${this.bottomActions.map(action => this.entrypointTemplate(action))}
+ </div>
+ </div>
+ `;
+ }
+}
+customElements.define("sidebar-main", SidebarMain);
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..aa1ae7e2e2
--- /dev/null
+++ b/browser/components/sidebar/sidebar-syncedtabs.html
@@ -0,0 +1,44 @@
+<!-- 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/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..e76fda863e
--- /dev/null
+++ b/browser/components/sidebar/sidebar.ftl
@@ -0,0 +1,33 @@
+# 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-main-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 }”
+
+sidebar-menu-customize =
+ .title = Customize sidebar
+sidebar-customize-header = Customize sidebar
+sidebar-customize-firefox-tools = { -brand-product-name } tools
+sidebar-customize-history = History
+sidebar-customize-synced-tabs = Tabs from other devices
diff --git a/browser/components/sidebar/tests/browser/browser.toml b/browser/components/sidebar/tests/browser/browser.toml
new file mode 100644
index 0000000000..5df032495c
--- /dev/null
+++ b/browser/components/sidebar/tests/browser/browser.toml
@@ -0,0 +1,7 @@
+[DEFAULT]
+
+["browser_customize_sidebar.js"]
+
+["browser_extensions_sidebar.js"]
+
+["browser_history_sidebar.js"]
diff --git a/browser/components/sidebar/tests/browser/browser_customize_sidebar.js b/browser/components/sidebar/tests/browser/browser_customize_sidebar.js
new file mode 100644
index 0000000000..ab26823d08
--- /dev/null
+++ b/browser/components/sidebar/tests/browser/browser_customize_sidebar.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(() => SpecialPowers.pushPrefEnv({ set: [["sidebar.revamp", true]] }));
+registerCleanupFunction(() => SpecialPowers.popPrefEnv());
+
+add_task(async function test_customize_sidebar_actions() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ const { document } = win;
+ const sidebar = document.getElementById("sidebar-main");
+ ok(sidebar, "Sidebar is shown.");
+
+ const button = sidebar.customizeButton;
+ const promiseFocused = BrowserTestUtils.waitForEvent(win, "SidebarFocused");
+ button.click();
+ await promiseFocused;
+ let customizeDocument = win.SidebarController.browser.contentDocument;
+ const customizeComponent =
+ customizeDocument.querySelector("sidebar-customize");
+ let toolEntrypointsCount = sidebar.toolButtons.length;
+ is(
+ customizeComponent.toolInputs.length,
+ toolEntrypointsCount,
+ `${toolEntrypointsCount} inputs to toggle Firefox Tools are shown in the Customize Menu.`
+ );
+ for (const toolInput of customizeComponent.toolInputs) {
+ toolInput.click();
+ await BrowserTestUtils.waitForCondition(() => {
+ let toggledTool = win.SidebarController.toolsAndExtensions.get(
+ toolInput.name
+ );
+ return toggledTool.disabled;
+ }, `The entrypoint for ${toolInput.name} has been disabled in the sidebar.`);
+ toolEntrypointsCount = sidebar.toolButtons.length;
+ is(
+ toolEntrypointsCount,
+ 1,
+ `The button for the ${toolInput.name} entrypoint has been removed.`
+ );
+ toolInput.click();
+ await BrowserTestUtils.waitForCondition(() => {
+ let toggledTool = win.SidebarController.toolsAndExtensions.get(
+ toolInput.name
+ );
+ return !toggledTool.disabled;
+ }, `The entrypoint for ${toolInput.name} has been re-enabled in the sidebar.`);
+ toolEntrypointsCount = sidebar.toolButtons.length;
+ is(
+ toolEntrypointsCount,
+ 2,
+ `The button for the ${toolInput.name} entrypoint has been added back.`
+ );
+ // Check ordering
+ is(
+ sidebar.toolButtons[1].getAttribute("view"),
+ toolInput.name,
+ `The button for the ${toolInput.name} entrypoint has been added back to the end of the list of tools/extensions entrypoints`
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/sidebar/tests/browser/browser_extensions_sidebar.js b/browser/components/sidebar/tests/browser/browser_extensions_sidebar.js
new file mode 100644
index 0000000000..0413849fd2
--- /dev/null
+++ b/browser/components/sidebar/tests/browser/browser_extensions_sidebar.js
@@ -0,0 +1,222 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(() => SpecialPowers.pushPrefEnv({ set: [["sidebar.revamp", true]] }));
+registerCleanupFunction(() => SpecialPowers.popPrefEnv());
+
+const imageBuffer = imageBufferFromDataURI(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=="
+);
+
+function imageBufferFromDataURI(encodedImageData) {
+ const decodedImageData = atob(encodedImageData);
+ return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer;
+}
+
+/* global browser */
+const extData = {
+ manifest: {
+ sidebar_action: {
+ default_icon: "default.png",
+ default_panel: "default.html",
+ default_title: "Default Title",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "default.html": `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"/>
+ <script src="sidebar.js"></script>
+ </head>
+ <body>
+ A Test Sidebar
+ </body></html>
+ `,
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ "1.html": `
+ <!DOCTYPE html>
+ <html>
+ <head><meta charset="utf-8"/></head>
+ <body>
+ A Test Sidebar
+ </body></html>
+ `,
+ "default.png": imageBuffer,
+ "1.png": imageBuffer,
+ },
+
+ background() {
+ browser.test.onMessage.addListener(async ({ msg, data }) => {
+ switch (msg) {
+ case "set-icon":
+ await browser.sidebarAction.setIcon({ path: data });
+ break;
+ case "set-panel":
+ await browser.sidebarAction.setPanel({ panel: data });
+ break;
+ case "set-title":
+ await browser.sidebarAction.setTitle({ title: data });
+ break;
+ }
+ browser.test.sendMessage("done");
+ });
+ },
+};
+
+async function sendMessage(extension, msg, data) {
+ extension.sendMessage({ msg, data });
+ await extension.awaitMessage("done");
+}
+
+add_task(async function test_extension_sidebar_actions() {
+ // TODO: Once `sidebar.revamp` is either enabled by default, or removed
+ // entirely, this test should run in the current window, and it should only
+ // await one "sidebar" message.
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ const { document } = win;
+ const sidebar = document.getElementById("sidebar-main");
+ ok(sidebar, "Sidebar is shown.");
+
+ const extension = ExtensionTestUtils.loadExtension({ ...extData });
+ await extension.startup();
+ await extension.awaitMessage("sidebar");
+ await extension.awaitMessage("sidebar");
+ is(sidebar.extensionButtons.length, 1, "Extension is shown in the sidebar.");
+
+ // Default icon and title matches.
+ const button = sidebar.extensionButtons[0];
+ let iconUrl = `moz-extension://${extension.uuid}/default.png`;
+ is(
+ button.style.getPropertyValue("--action-icon"),
+ `image-set(url("${iconUrl}"), url("${iconUrl}") 2x)`,
+ "Extension has the correct icon."
+ );
+ is(button.title, "Default Title", "Extension has the correct title.");
+
+ // Icon can be updated.
+ await sendMessage(extension, "set-icon", "1.png");
+ await sidebar.updateComplete;
+ iconUrl = `moz-extension://${extension.uuid}/1.png`;
+ is(
+ button.style.getPropertyValue("--action-icon"),
+ `image-set(url("${iconUrl}"), url("${iconUrl}") 2x)`,
+ "Extension has updated icon."
+ );
+
+ // Title can be updated.
+ await sendMessage(extension, "set-title", "Updated Title");
+ await sidebar.updateComplete;
+ is(button.title, "Updated Title", "Extension has updated title.");
+
+ // Panel can be updated.
+ await sendMessage(extension, "set-panel", "1.html");
+ const panelUrl = `moz-extension://${extension.uuid}/1.html`;
+ await TestUtils.waitForCondition(() => {
+ const browser = SidebarController.browser.contentDocument.getElementById(
+ "webext-panels-browser"
+ );
+ return browser.currentURI.spec === panelUrl;
+ }, "The new panel is visible.");
+
+ await extension.unload();
+ await sidebar.updateComplete;
+ is(
+ sidebar.extensionButtons.length,
+ 0,
+ "Extension is removed from the sidebar."
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_open_new_window_after_install() {
+ const extension = ExtensionTestUtils.loadExtension({ ...extData });
+ await extension.startup();
+ await extension.awaitMessage("sidebar");
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ const { document } = win;
+ const sidebar = document.getElementById("sidebar-main");
+ ok(sidebar, "Sidebar is shown.");
+ await extension.awaitMessage("sidebar");
+ is(
+ sidebar.extensionButtons.length,
+ 1,
+ "Extension is shown in new browser window."
+ );
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "about:addons" },
+ async browser => {
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "categories-box button[name=extension]",
+ {},
+ browser
+ );
+ const extensionToggle = await TestUtils.waitForCondition(
+ () =>
+ browser.contentDocument.querySelector(
+ `addon-card[addon-id="${extension.id}"] moz-toggle`
+ ),
+ "Toggle button for extension is shown."
+ );
+
+ let promiseEvent = BrowserTestUtils.waitForEvent(
+ win,
+ "SidebarItemRemoved"
+ );
+ extensionToggle.click();
+ await promiseEvent;
+ await sidebar.updateComplete;
+ is(sidebar.extensionButtons.length, 0, "The extension is disabled.");
+
+ promiseEvent = BrowserTestUtils.waitForEvent(win, "SidebarItemAdded");
+ extensionToggle.click();
+ await promiseEvent;
+ await sidebar.updateComplete;
+ is(sidebar.extensionButtons.length, 1, "The extension is enabled.");
+ }
+ );
+
+ await extension.unload();
+ await sidebar.updateComplete;
+ is(
+ sidebar.extensionButtons.length,
+ 0,
+ "Extension is removed from the sidebar."
+ );
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_open_new_private_window_after_install() {
+ const extension = ExtensionTestUtils.loadExtension({ ...extData });
+ await extension.startup();
+ await extension.awaitMessage("sidebar");
+
+ const privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ const { document } = privateWin;
+ const sidebar = document.getElementById("sidebar-main");
+ ok(sidebar, "Sidebar is shown.");
+ await TestUtils.waitForCondition(
+ () => sidebar.extensionButtons,
+ "Extensions container is shown."
+ );
+ is(
+ sidebar.extensionButtons.length,
+ 0,
+ "Extension is hidden in private browser window."
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(privateWin);
+});
diff --git a/browser/components/sidebar/tests/browser/browser_history_sidebar.js b/browser/components/sidebar/tests/browser/browser_history_sidebar.js
new file mode 100644
index 0000000000..a498eb4b8e
--- /dev/null
+++ b/browser/components/sidebar/tests/browser/browser_history_sidebar.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URLs = [
+ "http://mochi.test:8888/browser/",
+ "https://www.example.com/",
+ "https://example.net/",
+ "https://example.org/",
+];
+
+const today = new Date();
+const yesterday = new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate() - 1
+);
+const dates = [today, yesterday];
+
+let win;
+
+add_setup(async () => {
+ await SpecialPowers.pushPrefEnv({ set: [["sidebar.revamp", true]] });
+ const pageInfos = URLs.flatMap((url, i) =>
+ dates.map(date => ({
+ url,
+ title: `Example Domain ${i}`,
+ visits: [{ date }],
+ }))
+ );
+ await PlacesUtils.history.insertMany(pageInfos);
+ win = await BrowserTestUtils.openNewBrowserWindow();
+});
+
+registerCleanupFunction(async () => {
+ await SpecialPowers.popPrefEnv();
+ await PlacesUtils.history.clear();
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_history_cards_created() {
+ const { SidebarController } = win;
+ await SidebarController.show("viewHistorySidebar");
+ const document = SidebarController.browser.contentDocument;
+ const component = document.querySelector("sidebar-history");
+ await component.updateComplete;
+ const { lists } = component;
+
+ Assert.equal(lists.length, dates.length, "There is a card for each day.");
+ for (const list of lists) {
+ Assert.equal(
+ list.tabItems.length,
+ URLs.length,
+ "Card shows the correct number of visits."
+ );
+ }
+
+ SidebarController.hide();
+});
+
+add_task(async function test_history_search() {
+ const { SidebarController } = win;
+ await SidebarController.show("viewHistorySidebar");
+ const { contentDocument: document, contentWindow } =
+ SidebarController.browser;
+ const component = document.querySelector("sidebar-history");
+ await component.updateComplete;
+ const { searchTextbox } = component;
+
+ info("Input a search query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, contentWindow);
+ EventUtils.sendString("Example Domain 1", contentWindow);
+ await BrowserTestUtils.waitForMutationCondition(
+ component.shadowRoot,
+ { childList: true, subtree: true },
+ () =>
+ component.lists.length === 1 &&
+ component.shadowRoot.querySelector(
+ "moz-card[data-l10n-id=sidebar-search-results-header]"
+ )
+ );
+ await TestUtils.waitForCondition(() => {
+ const { rowEls } = component.lists[0];
+ return rowEls.length === 1 && rowEls[0].mainEl.href === URLs[1];
+ }, "There is one matching search result.");
+
+ info("Input a bogus search query.");
+ EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, contentWindow);
+ EventUtils.sendString("Bogus Query", contentWindow);
+ await TestUtils.waitForCondition(() => {
+ const tabList = component.lists[0];
+ return tabList?.emptyState;
+ }, "There are no matching search results.");
+
+ SidebarController.hide();
+});
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..276d3e06cf 100644
--- a/browser/components/storybook/.storybook/main.js
+++ b/browser/components/storybook/.storybook/main.js
@@ -13,17 +13,26 @@ 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`,
+ // Backup components stories
+ `${projectRoot}/browser/components/backup/content/**/*.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 +59,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.other-widgets.stories.md b/browser/components/storybook/docs/README.other-widgets.stories.md
index b2614d88b6..3c6f8d63f2 100644
--- a/browser/components/storybook/docs/README.other-widgets.stories.md
+++ b/browser/components/storybook/docs/README.other-widgets.stories.md
@@ -30,8 +30,8 @@ Please refer to the existing [UA widgets documentation](https://firefox-source-d
### How to use existing Mozilla Custom Elements
-The existing Mozilla Custom Elements are automatically imported into all chrome privileged documents.
-These existing elements do not need to be imported individually via `<script>` tag or by using `window.ensureCustomElements()` when in a privileged main process document.
+The existing Mozilla Custom Elements are either [automatically imported](https://searchfox.org/mozilla-central/rev/d23849dd6d83edbe681d3b4828700256ea34a654/toolkit/content/customElements.js#853-878) into all chrome privileged documents, or are [lazy loaded](https://searchfox.org/mozilla-central/rev/d23849dd6d83edbe681d3b4828700256ea34a654/toolkit/content/customElements.js#789-809) and get automatically imported when first used.
+In either case, these existing elements do not need to be imported individually via `<script>` tag.
As long as you are working in a chrome privileged document, you will have access to the existing Mozilla Custom Elements.
You can dynamically create one of the existing custom elements by using `document.createDocument("customElement)` or `document.createXULElement("customElement")` in the relevant JS file, or by using the custom element tag in the relevant XHTML document.
For example, `document.createXULElement("checkbox")` creates an instance of [widgets/checkbox.js](https://searchfox.org/mozilla-central/source/toolkit/content/widgets/checkbox.js) while using `<checkbox>` declares an instance in the XUL document.
@@ -74,11 +74,10 @@ Just like with the UI widgets, [the `browser_all_files_referenced.js` will fail
### Using new domain-specific widgets
-This is effectively the same as [using new design system components](#using-new-design-system-components).
-You will need to import your widget into the relevant `html`/`xhtml` files via a `script` tag with `type="module"`:
+This is effectively the same as [using new design system components](#using-new-design-system-components). In general you should be able to rely on these elements getting lazily loaded at the time of first use, similar to how existing custom elements are imported.
+
+Outside of chrome privileged documents you may need to import your widget into the relevant `html`/`xhtml` files via a `script` tag with `type="module"`:
```html
<script type="module" src="chrome://browser/content/<domain-directory>/<your-widget>.mjs"></script>
```
-
-Or use `window.ensureCustomElements("<your-widget>")` as previously stated in [the using new design system components section.](#using-new-design-system-components)
diff --git a/browser/components/storybook/docs/README.reusable-widgets.stories.md b/browser/components/storybook/docs/README.reusable-widgets.stories.md
index f26c18a2b0..003cbc36bf 100644
--- a/browser/components/storybook/docs/README.reusable-widgets.stories.md
+++ b/browser/components/storybook/docs/README.reusable-widgets.stories.md
@@ -16,6 +16,8 @@ re-rendering logic. All new components are being documented in Storybook in an
effort to create a catalog that engineers and designers can use to see which
components can be easily lifted off the shelf for use throughout Firefox.
+If you want to see the progress over time of these new reusable components, we have a [Reusable Component Adoption chart](https://firefoxux.github.io/recomp-metrics/) that you should check out!
+
## Designing new reusable widgets
Widgets that live at the global level, "UI Widgets", should be created in collaboration with the Design System team.
@@ -111,17 +113,16 @@ by updating [this array](https://searchfox.org/mozilla-central/rev/5c922d8b93b43
Once you've added a new component to `toolkit/content/widgets` and created
`chrome://` URLs via `toolkit/content/jar.mn` you should be able to start using it
-throughout Firefox. You can import the component into `html`/`xhtml` files via a
+throughout Firefox. In most cases, you should be able to rely on your custom element getting lazy loaded at the time of first use, provided you are working in a privileged context where `customElements.js` is available.
+
+You can import the component directly into `html`/`xhtml` files via a
`script` tag with `type="module"`:
```html
<script type="module" src="chrome://global/content/elements/your-component-name.mjs"></script>
```
-If you are unable to import the new component in html, you can use [`ensureCustomElements()` in customElements.js](https://searchfox.org/mozilla-central/rev/31f5847a4494b3646edabbdd7ea39cb88509afe2/toolkit/content/customElements.js#865) in the relevant JS file.
-For example, [we use `window.ensureCustomElements("moz-button-group")` in `browser-siteProtections.js`](https://searchfox.org/mozilla-central/rev/31f5847a4494b3646edabbdd7ea39cb88509afe2/browser/base/content/browser-siteProtections.js#1749).
-**Note** you will need to add your new widget to [the switch in importCustomElementFromESModule](https://searchfox.org/mozilla-central/rev/85b4f7363292b272eb9b606e00de2c37a6be73f0/toolkit/content/customElements.js#845-859) for `ensureCustomElements()` to work as expected.
-Once [Bug 1803810](https://bugzilla.mozilla.org/show_bug.cgi?id=1803810) lands, this process will be simplified: you won't need to use `ensureCustomElements()` and you will [add your widget to the appropriate array in customElements.js instead of the switch statement](https://searchfox.org/mozilla-central/rev/85b4f7363292b272eb9b606e00de2c37a6be73f0/toolkit/content/customElements.js#818-841).
+**Note** you will need to add your new widget to [this array in customElements.js](https://searchfox.org/mozilla-central/rev/cde3d4a8d228491e8b7f1bd94c63bbe039850696/toolkit/content/customElements.js#791-810) to ensure it gets lazy loaded on creation.
## Common pitfalls
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..888f9a567a 100644
--- a/browser/components/storybook/stories/fxview-tab-list.stories.mjs
+++ b/browser/components/storybook/stories/fxview-tab-list.stories.mjs
@@ -56,6 +56,7 @@ const Template = ({
.dateTimeFormat=${dateTimeFormat}
.maxTabsLength=${maxTabsLength}
.tabItems=${tabItems}
+ .updatesPaused=${false}
@fxview-tab-list-secondary-action=${secondaryAction}
@fxview-tab-list-primary-action=${primaryAction}
>
@@ -84,7 +85,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/storybook/stories/shopping-message-bar.stories.mjs b/browser/components/storybook/stories/shopping-message-bar.stories.mjs
index 7bc0895fd0..61b99d4d8d 100644
--- a/browser/components/storybook/stories/shopping-message-bar.stories.mjs
+++ b/browser/components/storybook/stories/shopping-message-bar.stories.mjs
@@ -15,21 +15,19 @@ export default {
component: "shopping-message-bar",
argTypes: {
type: {
- control: {
- type: "select",
- options: [
- "stale",
- "generic-error",
- "not-enough-reviews",
- "product-not-available",
- "product-not-available-reported",
- "thanks-for-reporting",
- "analysis-in-progress",
- "reanalysis-in-progress",
- "page-not-supported",
- "thank-you-for-feedback",
- ],
- },
+ control: { type: "select" },
+ options: [
+ "stale",
+ "generic-error",
+ "not-enough-reviews",
+ "product-not-available",
+ "product-not-available-reported",
+ "thanks-for-reporting",
+ "analysis-in-progress",
+ "reanalysis-in-progress",
+ "page-not-supported",
+ "thank-you-for-feedback",
+ ],
},
},
parameters: {
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..aa7ee439c3 100644
--- a/browser/components/syncedtabs/TabListView.sys.mjs
+++ b/browser/components/syncedtabs/TabListView.sys.mjs
@@ -5,6 +5,7 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
@@ -127,7 +128,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);
@@ -290,7 +291,7 @@ TabListView.prototype = {
// Middle click on a client
if (itemNode.classList.contains("client")) {
- let where = getChromeWindow(this._window).whereToOpenLink(event);
+ let where = lazy.BrowserUtils.whereToOpenLink(event);
if (where != "current") {
this._openAllClientTabs(itemNode, where);
}
@@ -346,7 +347,7 @@ TabListView.prototype = {
},
onOpenSelected(url, event) {
- let where = getChromeWindow(this._window).whereToOpenLink(event);
+ let where = lazy.BrowserUtils.whereToOpenLink(event);
this.props.onOpenTab(url, where, {});
},
diff --git a/browser/components/syncedtabs/sidebar.xhtml b/browser/components/syncedtabs/sidebar.xhtml
index 7790620f94..b5701450bd 100644
--- a/browser/components/syncedtabs/sidebar.xhtml
+++ b/browser/components/syncedtabs/sidebar.xhtml
@@ -22,7 +22,6 @@
href="chrome://browser/skin/syncedtabs/sidebar.css"
/>
<link rel="localization" href="browser/syncedTabs.ftl" />
- <link rel="localization" href="toolkit/branding/accounts.ftl" />
<title data-l10n-id="synced-tabs-sidebar-title" />
</head>
diff --git a/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js b/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
index daf980e5fa..2728ba5155 100644
--- a/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
+++ b/browser/components/syncedtabs/test/browser/browser_sidebar_syncedtabslist.js
@@ -91,28 +91,28 @@ function setupSyncedTabsStubs({
async function testClean() {
sinon.restore();
await new Promise(resolve => {
- window.SidebarUI.browser.contentWindow.addEventListener(
+ window.SidebarController.browser.contentWindow.addEventListener(
"unload",
function () {
resolve();
},
{ once: true }
);
- SidebarUI.hide();
+ SidebarController.hide();
});
}
add_task(async function testSyncedTabsSidebarList() {
- await SidebarUI.show("viewTabsSidebar");
+ await SidebarController.show("viewTabsSidebar");
Assert.equal(
- SidebarUI.currentID,
+ SidebarController.currentID,
"viewTabsSidebar",
"Sidebar should have SyncedTabs loaded"
);
let syncedTabsDeckComponent =
- SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
+ SidebarController.browser.contentWindow.syncedTabsDeckComponent;
Assert.ok(syncedTabsDeckComponent, "component exists");
@@ -172,9 +172,9 @@ add_task(async function testSyncedTabsSidebarList() {
add_task(testClean);
add_task(async function testSyncedTabsSidebarFilteredList() {
- await SidebarUI.show("viewTabsSidebar");
+ await SidebarController.show("viewTabsSidebar");
let syncedTabsDeckComponent =
- window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
+ window.SidebarController.browser.contentWindow.syncedTabsDeckComponent;
Assert.ok(syncedTabsDeckComponent, "component exists");
@@ -244,9 +244,9 @@ add_task(async function testSyncedTabsSidebarFilteredList() {
add_task(testClean);
add_task(async function testSyncedTabsSidebarStatus() {
- await SidebarUI.show("viewTabsSidebar");
+ await SidebarController.show("viewTabsSidebar");
let syncedTabsDeckComponent =
- window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
+ window.SidebarController.browser.contentWindow.syncedTabsDeckComponent;
Assert.ok(syncedTabsDeckComponent, "component exists");
@@ -377,9 +377,9 @@ add_task(async function testSyncedTabsSidebarStatus() {
add_task(testClean);
add_task(async function testSyncedTabsSidebarContextMenu() {
- await SidebarUI.show("viewTabsSidebar");
+ await SidebarController.show("viewTabsSidebar");
let syncedTabsDeckComponent =
- window.SidebarUI.browser.contentWindow.syncedTabsDeckComponent;
+ window.SidebarController.browser.contentWindow.syncedTabsDeckComponent;
Assert.ok(syncedTabsDeckComponent, "component exists");
@@ -547,7 +547,8 @@ async function testContextMenu(
let chromeWindow = triggerElement.ownerGlobal.top;
let rect = triggerElement.getBoundingClientRect();
- let contentRect = chromeWindow.SidebarUI.browser.getBoundingClientRect();
+ let contentRect =
+ chromeWindow.SidebarController.browser.getBoundingClientRect();
// The offsets in `rect` are relative to the content window, but
// `synthesizeMouseAtPoint` calls `nsIDOMWindowUtils.sendMouseEvent`,
// which interprets the offsets relative to the containing *chrome* window.
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..40874dbbf6
--- /dev/null
+++ b/browser/components/tabpreview/tab-preview-panel.mjs
@@ -0,0 +1,205 @@
+/* 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
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "_prefShowPidAndActiveness",
+ "browser.tabs.tooltipsShowPidAndActiveness",
+ 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;
+
+ if (this._prefShowPidAndActiveness) {
+ this._panel.querySelector(".tab-preview-pid").textContent =
+ this._displayPids;
+ this._panel.querySelector(".tab-preview-activeness").textContent =
+ this._displayActiveness;
+ } else {
+ this._panel.querySelector(".tab-preview-pid").textContent = "";
+ this._panel.querySelector(".tab-preview-activeness").textContent = "";
+ }
+
+ 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);
+ }
+
+ get _displayPids() {
+ const pids = this._win.gBrowser.getTabPids(this._tab);
+ if (!pids.length) {
+ return "";
+ }
+
+ let pidLabel = pids.length > 1 ? "pids" : "pid";
+ return `${pidLabel}: ${pids.join(", ")}`;
+ }
+
+ get _displayActiveness() {
+ return this._tab.linkedBrowser.docShellIsActive ? "[A]" : "";
+ }
+}
diff --git a/browser/components/tabpreview/tabpreview.css b/browser/components/tabpreview/tabpreview.css
index 776f520c7d..ad84f685a0 100644
--- a/browser/components/tabpreview/tabpreview.css
+++ b/browser/components/tabpreview/tabpreview.css
@@ -1,59 +1,48 @@
/* 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/. */
+ * 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;
+ pointer-events: none;
}
-.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;
+ -webkit-line-clamp: 2;
+ font-weight: var(--font-weight-bold);
}
.tab-preview-uri {
color: var(--text-color-deemphasized);
- max-height: 1.5em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
-.tab-preview-text-container {
- padding: 8px;
+.tab-preview-pid-activeness {
+ color: var(--text-color-deemphasized);
+ display: flex;
+ justify-content: space-between;
}
.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.toml b/browser/components/tests/browser/browser.toml
index c754191b8b..366842b04d 100644
--- a/browser/components/tests/browser/browser.toml
+++ b/browser/components/tests/browser/browser.toml
@@ -4,6 +4,9 @@ support-files = [
"../../../../dom/security/test/csp/dummy.pdf",
]
+["browser_browserGlue_os_auth.js"]
+skip-if = ["os == 'linux'"]
+
["browser_browserGlue_showModal_trigger.js"]
["browser_browserGlue_telemetry.js"]
diff --git a/browser/components/tests/browser/browser_browserGlue_os_auth.js b/browser/components/tests/browser/browser_browserGlue_os_auth.js
new file mode 100644
index 0000000000..9f17a00d03
--- /dev/null
+++ b/browser/components/tests/browser/browser_browserGlue_os_auth.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+// Check whether os auth is disabled by default on a new profile in Beta and Release.
+add_task(async function test_creditCards_os_auth_disabled_for_new_profile() {
+ Assert.equal(
+ FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ ),
+ AppConstants.NIGHTLY_BUILD,
+ "OS Auth should be disabled for credit cards by default for a new profile."
+ );
+
+ Assert.equal(
+ LoginHelper.getOSAuthEnabled(LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF),
+ AppConstants.NIGHTLY_BUILD,
+ "OS Auth should be disabled for passwords by default for a new profile."
+ );
+});
diff --git a/browser/components/tests/browser/browser_browserGlue_upgradeDialog_trigger.js b/browser/components/tests/browser/browser_browserGlue_upgradeDialog_trigger.js
index 88004525c8..f7b3e4c06f 100644
--- a/browser/components/tests/browser/browser_browserGlue_upgradeDialog_trigger.js
+++ b/browser/components/tests/browser/browser_browserGlue_upgradeDialog_trigger.js
@@ -122,83 +122,3 @@ add_task(async function show_major_upgrade() {
defaultPrefs.setBoolPref(pref, orig);
await cleanupUpgrade();
});
-
-add_task(async function test_mr2022_upgradeDialogEnabled() {
- const FALLBACK_PREF = "browser.startup.upgradeDialog.enabled";
-
- async function runMajorReleaseTest(
- { onboarding = undefined, enabled = undefined, fallbackPref = undefined },
- expected
- ) {
- info("Testing upgradeDialog with:");
- info(` majorRelease2022.onboarding=${onboarding}`);
- info(` upgradeDialog.enabled=${enabled}`);
- info(` ${FALLBACK_PREF}=${fallbackPref}`);
-
- let mr2022Cleanup = async () => {};
- let upgradeDialogCleanup = async () => {};
-
- if (typeof onboarding !== "undefined") {
- mr2022Cleanup = await ExperimentFakes.enrollWithFeatureConfig({
- featureId: "majorRelease2022",
- value: { onboarding },
- });
- }
-
- if (typeof enabled !== "undefined") {
- upgradeDialogCleanup = await ExperimentFakes.enrollWithFeatureConfig({
- featureId: "upgradeDialog",
- value: { enabled },
- });
- }
-
- if (typeof fallbackPref !== "undefined") {
- await SpecialPowers.pushPrefEnv({
- set: [[FALLBACK_PREF, fallbackPref]],
- });
- }
-
- const cleanupForcedUpgrade = await forceMajorUpgrade();
-
- try {
- await BROWSER_GLUE._maybeShowDefaultBrowserPrompt();
- AssertEvents(`Upgrade dialog ${expected ? "shown" : "not shown"}`, [
- "trigger",
- "reason",
- expected ? "satisfied" : "disabled",
- ]);
-
- if (expected) {
- const [win] = await TestUtils.topicObserved("subdialog-loaded");
- win.close();
- await BrowserTestUtils.removeTab(gBrowser.selectedTab);
- }
- } finally {
- await cleanupForcedUpgrade();
- if (typeof fallbackPref !== "undefined") {
- await SpecialPowers.popPrefEnv();
- }
- await upgradeDialogCleanup();
- await mr2022Cleanup();
- }
- }
-
- await runMajorReleaseTest({ onboarding: true }, true);
- await runMajorReleaseTest({ onboarding: true, enabled: false }, true);
- await runMajorReleaseTest({ onboarding: true, fallbackPref: false }, true);
-
- await runMajorReleaseTest({ onboarding: false }, false);
- await runMajorReleaseTest({ onboarding: false, enabled: true }, false);
- await runMajorReleaseTest({ onboarding: false, fallbackPref: true }, false);
-
- await runMajorReleaseTest({ enabled: true }, true);
- await runMajorReleaseTest({ enabled: true, fallbackPref: false }, true);
- await runMajorReleaseTest({ fallbackPref: true }, true);
-
- await runMajorReleaseTest({ enabled: false }, false);
- await runMajorReleaseTest({ enabled: false, fallbackPref: true }, false);
- await runMajorReleaseTest({ fallbackPref: false }, false);
-
- // Test the default configuration.
- await runMajorReleaseTest({}, false);
-});
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/tests/unit/test_browserGlue_migration_osauth.js b/browser/components/tests/unit/test_browserGlue_migration_osauth.js
new file mode 100644
index 0000000000..5676ea2fa9
--- /dev/null
+++ b/browser/components/tests/unit/test_browserGlue_migration_osauth.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_BROWSERGLUE_TEST = "force-ui-migration";
+const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+);
+const UI_VERSION = 147;
+
+const { LoginHelper } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginHelper.sys.mjs"
+);
+const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+const CC_OLD_PREF = "extensions.formautofill.reauth.enabled";
+const CC_TYPO_PREF = "extensions.formautofill.creditcards.reauth.optout";
+const CC_NEW_PREF = FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF;
+
+const PASSWORDS_OLD_PREF = "signon.management.page.os-auth.enabled";
+const PASSWORDS_NEW_PREF = LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF;
+
+function clearPrefs() {
+ Services.prefs.clearUserPref("browser.migration.version");
+ Services.prefs.clearUserPref(CC_OLD_PREF);
+ Services.prefs.clearUserPref(CC_TYPO_PREF);
+ Services.prefs.clearUserPref(CC_NEW_PREF);
+ Services.prefs.clearUserPref(PASSWORDS_OLD_PREF);
+ Services.prefs.clearUserPref(PASSWORDS_NEW_PREF);
+ Services.prefs.clearUserPref("browser.startup.homepage_override.mstone");
+}
+
+function simulateUIMigration() {
+ gBrowserGlue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_BROWSERGLUE_TEST
+ );
+}
+
+add_task(async function setup() {
+ registerCleanupFunction(clearPrefs);
+});
+
+add_task(async function test_pref_migration_old_pref_os_auth_disabled() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setBoolPref(CC_OLD_PREF, false);
+ Services.prefs.setBoolPref(PASSWORDS_OLD_PREF, false);
+
+ simulateUIMigration();
+
+ Assert.ok(
+ !FormAutofillUtils.getOSAuthEnabled(CC_NEW_PREF),
+ "OS Auth should be disabled for credit cards since it was disabled before migration."
+ );
+ Assert.ok(
+ !LoginHelper.getOSAuthEnabled(PASSWORDS_NEW_PREF),
+ "OS Auth should be disabled for passwords since it was disabled before migration."
+ );
+ clearPrefs();
+});
+
+add_task(async function test_pref_migration_old_pref_os_auth_enabled() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setBoolPref(CC_OLD_PREF, true);
+ Services.prefs.setBoolPref(PASSWORDS_OLD_PREF, true);
+
+ simulateUIMigration();
+
+ Assert.ok(
+ FormAutofillUtils.getOSAuthEnabled(CC_NEW_PREF),
+ "OS Auth should be enabled for credit cards since it was enabled before migration."
+ );
+ Assert.ok(
+ LoginHelper.getOSAuthEnabled(PASSWORDS_NEW_PREF),
+ "OS Auth should be enabled for passwords since it was enabled before migration."
+ );
+ clearPrefs();
+});
+
+add_task(
+ async function test_creditCards_pref_migration_typo_pref_os_auth_disabled() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setCharPref(
+ "browser.startup.homepage_override.mstone",
+ "127.0"
+ );
+ FormAutofillUtils.setOSAuthEnabled(CC_TYPO_PREF, false);
+
+ simulateUIMigration();
+
+ Assert.ok(
+ !FormAutofillUtils.getOSAuthEnabled(CC_NEW_PREF),
+ "OS Auth should be disabled for credit cards since it was disabled before migration."
+ );
+ clearPrefs();
+ }
+);
+
+add_task(
+ async function test_creditCards_pref_migration_typo_pref_os_auth_enabled() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setCharPref(
+ "browser.startup.homepage_override.mstone",
+ "127.0"
+ );
+ FormAutofillUtils.setOSAuthEnabled(CC_TYPO_PREF, true);
+
+ simulateUIMigration();
+
+ Assert.ok(
+ FormAutofillUtils.getOSAuthEnabled(CC_NEW_PREF),
+ "OS Auth should be enabled for credit cards since it was enabled before migration."
+ );
+ clearPrefs();
+ }
+);
+
+add_task(
+ async function test_creditCards_pref_migration_real_pref_os_auth_disabled() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setCharPref(
+ "browser.startup.homepage_override.mstone",
+ "127.0"
+ );
+ FormAutofillUtils.setOSAuthEnabled(CC_NEW_PREF, false);
+
+ simulateUIMigration();
+
+ Assert.ok(
+ !FormAutofillUtils.getOSAuthEnabled(CC_NEW_PREF),
+ "OS Auth should be disabled for credit cards since it was disabled before migration."
+ );
+ clearPrefs();
+ }
+);
+
+add_task(
+ async function test_creditCards_pref_migration_real_pref_os_auth_enabled() {
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION - 1);
+ Services.prefs.setCharPref(
+ "browser.startup.homepage_override.mstone",
+ "127.0"
+ );
+ FormAutofillUtils.setOSAuthEnabled(CC_NEW_PREF, true);
+
+ simulateUIMigration();
+
+ Assert.ok(
+ FormAutofillUtils.getOSAuthEnabled(CC_NEW_PREF),
+ "OS Auth should be enabled for credit cards since it was enabled before migration."
+ );
+ clearPrefs();
+ }
+);
diff --git a/browser/components/tests/unit/xpcshell.toml b/browser/components/tests/unit/xpcshell.toml
index 1b566698ee..b552fd2fa8 100644
--- a/browser/components/tests/unit/xpcshell.toml
+++ b/browser/components/tests/unit/xpcshell.toml
@@ -10,6 +10,9 @@ support-files = ["distribution.ini"]
["test_browserGlue_migration_no_errors.js"]
+["test_browserGlue_migration_osauth.js"]
+skip-if = ["nightly_build", "os == 'linux'"]
+
["test_browserGlue_migration_places_xulstore.js"]
["test_browserGlue_migration_remove_pref.js"]
diff --git a/browser/components/topsites/TopSites.sys.mjs b/browser/components/topsites/TopSites.sys.mjs
new file mode 100644
index 0000000000..736a079f34
--- /dev/null
+++ b/browser/components/topsites/TopSites.sys.mjs
@@ -0,0 +1,2039 @@
+/* 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 {
+ actionCreators as ac,
+ actionTypes as at,
+} from "resource://activity-stream/common/Actions.mjs";
+import { TippyTopProvider } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs";
+import {
+ insertPinned,
+ TOP_SITES_MAX_SITES_PER_ROW,
+} from "resource://activity-stream/common/Reducers.sys.mjs";
+import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs";
+import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs";
+import { getDefaultOptions } from "resource://activity-stream/lib/ActivityStreamStorage.sys.mjs";
+
+import {
+ CUSTOM_SEARCH_SHORTCUTS,
+ SEARCH_SHORTCUTS_EXPERIMENT,
+ SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF,
+ SEARCH_SHORTCUTS_HAVE_PINNED_PREF,
+ checkHasSearchEngine,
+ getSearchProvider,
+ getSearchFormURL,
+} from "resource://activity-stream/lib/SearchShortcuts.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs",
+ LinksCache: "resource://activity-stream/lib/LinksCache.sys.mjs",
+ NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
+ Region: "resource://gre/modules/Region.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ Sampling: "resource://gre/modules/components-utils/Sampling.sys.mjs",
+ Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "log", () => {
+ const { Logger } = ChromeUtils.importESModule(
+ "resource://messaging-system/lib/Logger.sys.mjs"
+ );
+ return new Logger("TopSites");
+});
+
+// `contextId` is a unique identifier used by Contextual Services
+const CONTEXT_ID_PREF = "browser.contextual-services.contextId";
+ChromeUtils.defineLazyGetter(lazy, "contextId", () => {
+ let _contextId = Services.prefs.getStringPref(CONTEXT_ID_PREF, null);
+ if (!_contextId) {
+ _contextId = String(Services.uuid.generateUUID());
+ Services.prefs.setStringPref(CONTEXT_ID_PREF, _contextId);
+ }
+ return _contextId;
+});
+
+const DEFAULT_SITES_PREF = "default.sites";
+const SHOWN_ON_NEWTAB_PREF = "feeds.topsites";
+export const DEFAULT_TOP_SITES = [];
+const FRECENCY_THRESHOLD = 100 + 1; // 1 visit (skip first-run/one-time pages)
+const MIN_FAVICON_SIZE = 96;
+const CACHED_LINK_PROPS_TO_MIGRATE = ["screenshot", "customScreenshot"];
+const PINNED_FAVICON_PROPS_TO_MIGRATE = [
+ "favicon",
+ "faviconRef",
+ "faviconSize",
+];
+const SECTION_ID = "topsites";
+const ROWS_PREF = "topSitesRows";
+const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
+// The default total number of sponsored top sites to fetch from Contile
+// and Pocket.
+const MAX_NUM_SPONSORED = 2;
+// Nimbus variable for the total number of sponsored top sites including
+// both Contile and Pocket sources.
+// The default will be `MAX_NUM_SPONSORED` if this variable is unspecified.
+const NIMBUS_VARIABLE_MAX_SPONSORED = "topSitesMaxSponsored";
+// Nimbus variable to allow more than two sponsored tiles from Contile to be
+//considered for Top Sites.
+const NIMBUS_VARIABLE_ADDITIONAL_TILES =
+ "topSitesUseAdditionalTilesFromContile";
+// Nimbus variable to enable the SOV feature for sponsored tiles.
+const NIMBUS_VARIABLE_CONTILE_SOV_ENABLED = "topSitesContileSovEnabled";
+// Nimbu variable for the total number of sponsor topsite that come from Contile
+// The default will be `CONTILE_MAX_NUM_SPONSORED` if variable is unspecified.
+const NIMBUS_VARIABLE_CONTILE_MAX_NUM_SPONSORED = "topSitesContileMaxSponsored";
+
+// Search experiment stuff
+const FILTER_DEFAULT_SEARCH_PREF = "improvesearch.noDefaultSearchTile";
+const SEARCH_FILTERS = [
+ "google",
+ "search.yahoo",
+ "yahoo",
+ "bing",
+ "ask",
+ "duckduckgo",
+];
+
+const REMOTE_SETTING_DEFAULTS_PREF = "browser.topsites.useRemoteSetting";
+const DEFAULT_SITES_OVERRIDE_PREF =
+ "browser.newtabpage.activity-stream.default.sites";
+const DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH = "browser.topsites.experiment.";
+
+// Mozilla Tiles Service (Contile) prefs
+// Nimbus variable for the Contile integration. It falls back to the pref:
+// `browser.topsites.contile.enabled`.
+const NIMBUS_VARIABLE_CONTILE_ENABLED = "topSitesContileEnabled";
+const NIMBUS_VARIABLE_CONTILE_POSITIONS = "contileTopsitesPositions";
+const CONTILE_ENDPOINT_PREF = "browser.topsites.contile.endpoint";
+const CONTILE_UPDATE_INTERVAL = 15 * 60 * 1000; // 15 minutes
+// The maximum number of sponsored top sites to fetch from Contile.
+const CONTILE_MAX_NUM_SPONSORED = 2;
+const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
+const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles";
+const CONTILE_CACHE_VALID_FOR_PREF = "browser.topsites.contile.cacheValidFor";
+const CONTILE_CACHE_LAST_FETCH_PREF = "browser.topsites.contile.lastFetch";
+
+// Partners of sponsored tiles.
+const SPONSORED_TILE_PARTNER_AMP = "amp";
+const SPONSORED_TILE_PARTNER_MOZ_SALES = "moz-sales";
+const SPONSORED_TILE_PARTNERS = new Set([
+ SPONSORED_TILE_PARTNER_AMP,
+ SPONSORED_TILE_PARTNER_MOZ_SALES,
+]);
+
+const DISPLAY_FAIL_REASON_OVERSOLD = "oversold";
+const DISPLAY_FAIL_REASON_DISMISSED = "dismissed";
+const DISPLAY_FAIL_REASON_UNRESOLVED = "unresolved";
+
+function getShortURLForCurrentSearch() {
+ const url = shortURL({ url: Services.search.defaultEngine.searchForm });
+ return url;
+}
+
+class TopSitesTelemetry {
+ constructor() {
+ this.allSponsoredTiles = {};
+ this.sponsoredTilesConfigured = 0;
+ }
+
+ _tileProviderForTiles(tiles) {
+ // Assumption: the list of tiles is from a single provider
+ return tiles && tiles.length ? this._tileProvider(tiles[0]) : null;
+ }
+
+ _tileProvider(tile) {
+ return tile.partner || SPONSORED_TILE_PARTNER_AMP;
+ }
+
+ _buildPropertyKey(tile) {
+ let provider = this._tileProvider(tile);
+ return provider + shortURL(tile);
+ }
+
+ // Returns an array of strings indicating the property name (based on the
+ // provider and brand) of tiles that have been filtered e.g. ["moz-salesbrand1"]
+ // currentTiles: The list of tiles remaining and may be displayed in new tab.
+ // this.allSponsoredTiles: The original list of tiles set via setTiles prior to any filtering
+ // The returned list indicated the difference between these two lists (excluding any previously filtered tiles).
+ _getFilteredTiles(currentTiles) {
+ let notPreviouslyFilteredTiles = Object.assign(
+ {},
+ ...Object.entries(this.allSponsoredTiles)
+ .filter(
+ ([, v]) =>
+ v.display_fail_reason === null ||
+ v.display_fail_reason === undefined
+ )
+ .map(([k, v]) => ({ [k]: v }))
+ );
+
+ // Get the property names of the newly filtered list.
+ let remainingTiles = currentTiles.map(el => {
+ return this._buildPropertyKey(el);
+ });
+
+ // Get the property names of the tiles that were filtered.
+ let tilesToUpdate = Object.keys(notPreviouslyFilteredTiles).filter(
+ element => !remainingTiles.includes(element)
+ );
+ return tilesToUpdate;
+ }
+
+ setSponsoredTilesConfigured() {
+ const maxSponsored =
+ lazy.NimbusFeatures.pocketNewtab.getVariable(
+ NIMBUS_VARIABLE_MAX_SPONSORED
+ ) ?? MAX_NUM_SPONSORED;
+
+ this.sponsoredTilesConfigured = maxSponsored;
+ Glean.topsites.sponsoredTilesConfigured.set(maxSponsored);
+ }
+
+ clearTilesForProvider(provider) {
+ Object.entries(this.allSponsoredTiles)
+ .filter(([k]) => k.startsWith(provider))
+ .map(([k]) => delete this.allSponsoredTiles[k]);
+ }
+
+ _getAdvertiser(tile) {
+ let label = tile.label || null;
+ let title = tile.title || null;
+
+ return label ?? title ?? shortURL(tile);
+ }
+
+ setTiles(tiles) {
+ // Assumption: the list of tiles is from a single provider,
+ // should be called once per tile source.
+ if (tiles && tiles.length) {
+ let tile_provider = this._tileProviderForTiles(tiles);
+ this.clearTilesForProvider(tile_provider);
+
+ for (let sponsoredTile of tiles) {
+ this.allSponsoredTiles[this._buildPropertyKey(sponsoredTile)] = {
+ advertiser: this._getAdvertiser(sponsoredTile).toLowerCase(),
+ provider: tile_provider,
+ display_position: null,
+ display_fail_reason: null,
+ };
+ }
+ }
+ }
+
+ _setDisplayFailReason(filteredTiles, reason) {
+ for (let tile of filteredTiles) {
+ if (tile in this.allSponsoredTiles) {
+ let tileToUpdate = this.allSponsoredTiles[tile];
+ tileToUpdate.display_position = null;
+ tileToUpdate.display_fail_reason = reason;
+ }
+ }
+ }
+
+ determineFilteredTilesAndSetToOversold(nonOversoldTiles) {
+ let filteredTiles = this._getFilteredTiles(nonOversoldTiles);
+ this._setDisplayFailReason(filteredTiles, DISPLAY_FAIL_REASON_OVERSOLD);
+ }
+
+ determineFilteredTilesAndSetToDismissed(nonDismissedTiles) {
+ let filteredTiles = this._getFilteredTiles(nonDismissedTiles);
+ this._setDisplayFailReason(filteredTiles, DISPLAY_FAIL_REASON_DISMISSED);
+ }
+
+ _setTilePositions(currentTiles) {
+ // This function performs many loops over a small dataset. The size of
+ // dataset is limited by the number of sponsored tiles displayed on
+ // the newtab instance.
+ if (this.allSponsoredTiles) {
+ let tilePositionsAssigned = [];
+ // processing the currentTiles parameter, assigns a position to the
+ // corresponding property in this.allSponsoredTiles
+ currentTiles.forEach(item => {
+ let tile = this.allSponsoredTiles[this._buildPropertyKey(item)];
+ if (
+ tile &&
+ (tile.display_fail_reason === undefined ||
+ tile.display_fail_reason === null)
+ ) {
+ tile.display_position = item.sponsored_position;
+ // Track assigned tile slots.
+ tilePositionsAssigned.push(item.sponsored_position);
+ }
+ });
+
+ // Need to check if any objects in this.allSponsoredTiles do not
+ // have either a display_fail_reason or a display_position set.
+ // This can happen if the tiles list was updated before the
+ // metric is written to Glean.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1877197
+ let tilesMissingPosition = [];
+ Object.keys(this.allSponsoredTiles).forEach(property => {
+ let tile = this.allSponsoredTiles[property];
+ if (!tile.display_fail_reason && !tile.display_position) {
+ tilesMissingPosition.push(property);
+ }
+ });
+
+ if (tilesMissingPosition.length) {
+ // Determine if any available slots exist based on max number of tiles
+ // and the list of tiles already used and assign to a tile with missing
+ // value.
+ for (let i = 1; i <= this.sponsoredTilesConfigured; i++) {
+ if (!tilePositionsAssigned.includes(i)) {
+ let tileProperty = tilesMissingPosition.shift();
+ this.allSponsoredTiles[tileProperty].display_position = i;
+ }
+ }
+ }
+
+ // At this point we might still have a few unresolved states. These
+ // rows will be tagged with a display_fail_reason `unresolved`.
+ this._detectErrorConditionAndSetUnresolved();
+ }
+ }
+
+ // Checks the data for inconsistent state and updates the display_fail_reason
+ _detectErrorConditionAndSetUnresolved() {
+ Object.keys(this.allSponsoredTiles).forEach(property => {
+ let tile = this.allSponsoredTiles[property];
+ if (
+ (!tile.display_fail_reason && !tile.display_position) ||
+ (tile.display_fail_reason && tile.display_position)
+ ) {
+ tile.display_position = null;
+ tile.display_fail_reason = DISPLAY_FAIL_REASON_UNRESOLVED;
+ }
+ });
+ }
+
+ finalizeNewtabPingFields(currentTiles) {
+ this._setTilePositions(currentTiles);
+ Glean.topsites.sponsoredTilesReceived.set(
+ JSON.stringify({
+ sponsoredTilesReceived: Object.values(this.allSponsoredTiles),
+ })
+ );
+ }
+}
+
+export class ContileIntegration {
+ constructor(topSitesFeed) {
+ this._topSitesFeed = topSitesFeed;
+ this._lastPeriodicUpdate = 0;
+ this._sites = [];
+ // The Share-of-Voice object managed by Shepherd and sent via Contile.
+ this._sov = null;
+ }
+
+ get sites() {
+ return this._sites;
+ }
+
+ get sov() {
+ return this._sov;
+ }
+
+ periodicUpdate() {
+ let now = Date.now();
+ if (now - this._lastPeriodicUpdate >= CONTILE_UPDATE_INTERVAL) {
+ this._lastPeriodicUpdate = now;
+ this.refresh();
+ }
+ }
+
+ async refresh() {
+ let updateDefaultSites = await this._fetchSites();
+ await this._topSitesFeed.allocatePositions();
+ if (updateDefaultSites) {
+ this._topSitesFeed._readDefaults();
+ }
+ }
+
+ /**
+ * Clear Contile Cache Prefs.
+ */
+ _resetContileCachePrefs() {
+ Services.prefs.clearUserPref(CONTILE_CACHE_PREF);
+ Services.prefs.clearUserPref(CONTILE_CACHE_LAST_FETCH_PREF);
+ Services.prefs.clearUserPref(CONTILE_CACHE_VALID_FOR_PREF);
+ }
+
+ /**
+ * Filter the tiles whose sponsor is on the Top Sites sponsor blocklist.
+ *
+ * @param {Array} tiles
+ * An array of the tile objects
+ */
+ _filterBlockedSponsors(tiles) {
+ const blocklist = JSON.parse(
+ Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF, "[]")
+ );
+ return tiles.filter(tile => !blocklist.includes(shortURL(tile)));
+ }
+
+ /**
+ * Calculate the time Contile response is valid for based on cache-control header
+ *
+ * @param {string} cacheHeader
+ * string value of the Contile resposne cache-control header
+ */
+ _extractCacheValidFor(cacheHeader) {
+ if (!cacheHeader) {
+ lazy.log.warn("Contile response cache control header is empty");
+ return 0;
+ }
+ const [, staleIfError] = cacheHeader.match(/stale-if-error=\s*([0-9]+)/i);
+ const [, maxAge] = cacheHeader.match(/max-age=\s*([0-9]+)/i);
+ const validFor =
+ Number.parseInt(staleIfError, 10) + Number.parseInt(maxAge, 10);
+ return isNaN(validFor) ? 0 : validFor;
+ }
+
+ /**
+ * Load Tiles from Contile Cache Prefs
+ */
+ _loadTilesFromCache() {
+ lazy.log.info("Contile client is trying to load tiles from local cache.");
+ const now = Math.round(Date.now() / 1000);
+ const lastFetch = Services.prefs.getIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ 0
+ );
+ const validFor = Services.prefs.getIntPref(CONTILE_CACHE_VALID_FOR_PREF, 0);
+ this._topSitesFeed._telemetryUtility.setSponsoredTilesConfigured();
+ if (now <= lastFetch + validFor) {
+ try {
+ let cachedTiles = JSON.parse(
+ Services.prefs.getStringPref(CONTILE_CACHE_PREF)
+ );
+ this._topSitesFeed._telemetryUtility.setTiles(cachedTiles);
+ cachedTiles = this._filterBlockedSponsors(cachedTiles);
+ this._topSitesFeed._telemetryUtility.determineFilteredTilesAndSetToDismissed(
+ cachedTiles
+ );
+ this._sites = cachedTiles;
+ lazy.log.info("Local cache loaded.");
+ return true;
+ } catch (error) {
+ lazy.log.warn(`Failed to load tiles from local cache: ${error}.`);
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Determine number of Tiles to get from Contile
+ */
+ _getMaxNumFromContile() {
+ return (
+ lazy.NimbusFeatures.pocketNewtab.getVariable(
+ NIMBUS_VARIABLE_CONTILE_MAX_NUM_SPONSORED
+ ) ?? CONTILE_MAX_NUM_SPONSORED
+ );
+ }
+
+ async _fetchSites() {
+ if (
+ !lazy.NimbusFeatures.newtab.getVariable(
+ NIMBUS_VARIABLE_CONTILE_ENABLED
+ ) ||
+ !this._topSitesFeed.store.getState().Prefs.values[SHOW_SPONSORED_PREF]
+ ) {
+ if (this._sites.length) {
+ this._sites = [];
+ return true;
+ }
+ return false;
+ }
+ try {
+ let url = Services.prefs.getStringPref(CONTILE_ENDPOINT_PREF);
+ const response = await this._topSitesFeed.fetch(url, {
+ credentials: "omit",
+ });
+ if (!response.ok) {
+ lazy.log.warn(
+ `Contile endpoint returned unexpected status: ${response.status}`
+ );
+ if (response.status === 304 || response.status >= 500) {
+ return this._loadTilesFromCache();
+ }
+ }
+
+ const lastFetch = Math.round(Date.now() / 1000);
+ Services.prefs.setIntPref(CONTILE_CACHE_LAST_FETCH_PREF, lastFetch);
+ this._topSitesFeed._telemetryUtility.setSponsoredTilesConfigured();
+
+ // Contile returns 204 indicating there is no content at the moment.
+ // If this happens, it will clear `this._sites` reset the cached tiles
+ // to an empty array.
+ if (response.status === 204) {
+ this._topSitesFeed._telemetryUtility.clearTilesForProvider(
+ SPONSORED_TILE_PARTNER_AMP
+ );
+ if (this._sites.length) {
+ this._sites = [];
+ Services.prefs.setStringPref(
+ CONTILE_CACHE_PREF,
+ JSON.stringify(this._sites)
+ );
+ return true;
+ }
+ return false;
+ }
+ const body = await response.json();
+
+ if (body?.sov) {
+ this._sov = JSON.parse(atob(body.sov));
+ }
+ if (body?.tiles && Array.isArray(body.tiles)) {
+ const useAdditionalTiles = lazy.NimbusFeatures.newtab.getVariable(
+ NIMBUS_VARIABLE_ADDITIONAL_TILES
+ );
+
+ const maxNumFromContile = this._getMaxNumFromContile();
+
+ let { tiles } = body;
+ this._topSitesFeed._telemetryUtility.setTiles(tiles);
+ if (
+ useAdditionalTiles !== undefined &&
+ !useAdditionalTiles &&
+ tiles.length > maxNumFromContile
+ ) {
+ tiles.length = maxNumFromContile;
+ this._topSitesFeed._telemetryUtility.determineFilteredTilesAndSetToOversold(
+ tiles
+ );
+ }
+ tiles = this._filterBlockedSponsors(tiles);
+ this._topSitesFeed._telemetryUtility.determineFilteredTilesAndSetToDismissed(
+ tiles
+ );
+ if (tiles.length > maxNumFromContile) {
+ lazy.log.info("Remove unused links from Contile");
+ tiles.length = maxNumFromContile;
+ this._topSitesFeed._telemetryUtility.determineFilteredTilesAndSetToOversold(
+ tiles
+ );
+ }
+ this._sites = tiles;
+ Services.prefs.setStringPref(
+ CONTILE_CACHE_PREF,
+ JSON.stringify(this._sites)
+ );
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_VALID_FOR_PREF,
+ this._extractCacheValidFor(
+ response.headers.get("cache-control") ||
+ response.headers.get("Cache-Control")
+ )
+ );
+
+ return true;
+ }
+ } catch (error) {
+ lazy.log.warn(
+ `Failed to fetch data from Contile server: ${error.message}`
+ );
+ return this._loadTilesFromCache();
+ }
+ return false;
+ }
+}
+
+class _TopSites {
+ constructor() {
+ this._telemetryUtility = new TopSitesTelemetry();
+ this._contile = new ContileIntegration(this);
+ this._tippyTopProvider = new TippyTopProvider();
+ ChromeUtils.defineLazyGetter(
+ this,
+ "_currentSearchHostname",
+ getShortURLForCurrentSearch
+ );
+ this.dedupe = new Dedupe(this._dedupeKey);
+ this.frecentCache = new lazy.LinksCache(
+ lazy.NewTabUtils.activityStreamLinks,
+ "getTopSites",
+ CACHED_LINK_PROPS_TO_MIGRATE,
+ (oldOptions, newOptions) =>
+ // Refresh if no old options or requesting more items
+ !(oldOptions.numItems >= newOptions.numItems)
+ );
+ this.pinnedCache = new lazy.LinksCache(
+ lazy.NewTabUtils.pinnedLinks,
+ "links",
+ [...CACHED_LINK_PROPS_TO_MIGRATE, ...PINNED_FAVICON_PROPS_TO_MIGRATE]
+ );
+ lazy.PageThumbs.addExpirationFilter(this);
+ this._nimbusChangeListener = this._nimbusChangeListener.bind(this);
+ }
+
+ _nimbusChangeListener(event, reason) {
+ // The Nimbus API current doesn't specify the changed variable(s) in the
+ // listener callback, so we have to refresh unconditionally on every change
+ // of the `newtab` feature. It should be a manageable overhead given the
+ // current update cadence (6 hours) of Nimbus.
+ //
+ // Skip the experiment and rollout loading reasons since this feature has
+ // `isEarlyStartup` enabled, the feature variables are already available
+ // before the experiment or rollout loads.
+ if (
+ !["feature-experiment-loaded", "feature-rollout-loaded"].includes(reason)
+ ) {
+ this._contile.refresh();
+ }
+ }
+
+ init() {
+ // If the feed was previously disabled PREFS_INITIAL_VALUES was never received
+ this._readDefaults({ isStartup: true });
+ this._storage = this.store.dbStorage.getDbTable("sectionPrefs");
+ this._contile.refresh();
+ Services.obs.addObserver(this, "browser-search-engine-modified");
+ Services.obs.addObserver(this, "browser-region-updated");
+ Services.prefs.addObserver(REMOTE_SETTING_DEFAULTS_PREF, this);
+ Services.prefs.addObserver(DEFAULT_SITES_OVERRIDE_PREF, this);
+ Services.prefs.addObserver(DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH, this);
+ lazy.NimbusFeatures.newtab.onUpdate(this._nimbusChangeListener);
+ }
+
+ uninit() {
+ lazy.PageThumbs.removeExpirationFilter(this);
+ Services.obs.removeObserver(this, "browser-search-engine-modified");
+ Services.obs.removeObserver(this, "browser-region-updated");
+ Services.prefs.removeObserver(REMOTE_SETTING_DEFAULTS_PREF, this);
+ Services.prefs.removeObserver(DEFAULT_SITES_OVERRIDE_PREF, this);
+ Services.prefs.removeObserver(DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH, this);
+ lazy.NimbusFeatures.newtab.offUpdate(this._nimbusChangeListener);
+ }
+
+ observe(subj, topic, data) {
+ switch (topic) {
+ case "browser-search-engine-modified":
+ // We should update the current top sites if the search engine has been changed since
+ // the search engine that gets filtered out of top sites has changed.
+ // We also need to drop search shortcuts when their engine gets removed / hidden.
+ if (
+ data === "engine-default" &&
+ this.store.getState().Prefs.values[FILTER_DEFAULT_SEARCH_PREF]
+ ) {
+ delete this._currentSearchHostname;
+ this._currentSearchHostname = getShortURLForCurrentSearch();
+ }
+ this.refresh({ broadcast: true });
+ break;
+ case "browser-region-updated":
+ this._readDefaults();
+ break;
+ case "nsPref:changed":
+ if (
+ data === REMOTE_SETTING_DEFAULTS_PREF ||
+ data === DEFAULT_SITES_OVERRIDE_PREF ||
+ data.startsWith(DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH)
+ ) {
+ this._readDefaults();
+ }
+ break;
+ }
+ }
+
+ _dedupeKey(site) {
+ return site && site.hostname;
+ }
+
+ /**
+ * _readDefaults - sets DEFAULT_TOP_SITES
+ */
+ async _readDefaults({ isStartup = false } = {}) {
+ this._useRemoteSetting = false;
+
+ if (!Services.prefs.getBoolPref(REMOTE_SETTING_DEFAULTS_PREF)) {
+ this.refreshDefaults(
+ this.store.getState().Prefs.values[DEFAULT_SITES_PREF],
+ { isStartup }
+ );
+ return;
+ }
+
+ // Try using default top sites from enterprise policies or tests. The pref
+ // is locked when set via enterprise policy. Tests have no default sites
+ // unless they set them via this pref.
+ if (
+ Services.prefs.prefIsLocked(DEFAULT_SITES_OVERRIDE_PREF) ||
+ Cu.isInAutomation
+ ) {
+ let sites = Services.prefs.getStringPref(DEFAULT_SITES_OVERRIDE_PREF, "");
+ this.refreshDefaults(sites, { isStartup });
+ return;
+ }
+
+ // Clear out the array of any previous defaults.
+ DEFAULT_TOP_SITES.length = 0;
+
+ // Read defaults from contile.
+ const contileEnabled = lazy.NimbusFeatures.newtab.getVariable(
+ NIMBUS_VARIABLE_CONTILE_ENABLED
+ );
+
+ // Keep the number of positions in the array in sync with CONTILE_MAX_NUM_SPONSORED.
+ // sponsored_position is a 1-based index, and contilePositions is a 0-based index,
+ // so we need to add 1 to each of these.
+ // Also currently this does not work with SOV.
+ let contilePositions = lazy.NimbusFeatures.pocketNewtab
+ .getVariable(NIMBUS_VARIABLE_CONTILE_POSITIONS)
+ ?.split(",")
+ .map(item => parseInt(item, 10) + 1)
+ .filter(item => !Number.isNaN(item));
+ if (!contilePositions || contilePositions.length === 0) {
+ contilePositions = [1, 2];
+ }
+
+ let hasContileTiles = false;
+ if (contileEnabled) {
+ let contilePositionIndex = 0;
+ // We need to loop through potential spocs and set their positions.
+ // If we run out of spocs or positions, we stop.
+ // First, we need to know which array is shortest. This is our exit condition.
+ const minLength = Math.min(
+ contilePositions.length,
+ this._contile.sites.length
+ );
+ // Loop until we run out of spocs or positions.
+ for (let i = 0; i < minLength; i++) {
+ let site = this._contile.sites[i];
+ let hostname = shortURL(site);
+ let link = {
+ isDefault: true,
+ url: site.url,
+ hostname,
+ sendAttributionRequest: false,
+ label: site.name,
+ show_sponsored_label: hostname !== "yandex",
+ sponsored_position: contilePositions[contilePositionIndex++],
+ sponsored_click_url: site.click_url,
+ sponsored_impression_url: site.impression_url,
+ sponsored_tile_id: site.id,
+ partner: SPONSORED_TILE_PARTNER_AMP,
+ };
+ if (site.image_url && site.image_size >= MIN_FAVICON_SIZE) {
+ // Only use the image from Contile if it's hi-res, otherwise, fallback
+ // to the built-in favicons.
+ link.favicon = site.image_url;
+ link.faviconSize = site.image_size;
+ }
+ DEFAULT_TOP_SITES.push(link);
+ }
+ hasContileTiles = contilePositionIndex > 0;
+ //This is to catch where we receive 3 tiles but reduce to 2 early in the filtering, before blocked list applied.
+ this._telemetryUtility.determineFilteredTilesAndSetToOversold(
+ DEFAULT_TOP_SITES
+ );
+ }
+
+ // Read defaults from remote settings.
+ this._useRemoteSetting = true;
+ let remoteSettingData = await this._getRemoteConfig();
+
+ const sponsoredBlocklist = JSON.parse(
+ Services.prefs.getStringPref(TOP_SITES_BLOCKED_SPONSORS_PREF, "[]")
+ );
+
+ for (let siteData of remoteSettingData) {
+ let hostname = shortURL(siteData);
+ // Drop default sites when Contile already provided a sponsored one with
+ // the same host name.
+ if (
+ contileEnabled &&
+ DEFAULT_TOP_SITES.findIndex(site => site.hostname === hostname) > -1
+ ) {
+ continue;
+ }
+ // Also drop those sponsored sites that were blocked by the user before
+ // with the same hostname.
+ if (
+ siteData.sponsored_position &&
+ sponsoredBlocklist.includes(hostname)
+ ) {
+ continue;
+ }
+ let link = {
+ isDefault: true,
+ url: siteData.url,
+ hostname,
+ sendAttributionRequest: !!siteData.send_attribution_request,
+ };
+ if (siteData.url_urlbar_override) {
+ link.url_urlbar = siteData.url_urlbar_override;
+ }
+ if (siteData.title) {
+ link.label = siteData.title;
+ }
+ if (siteData.search_shortcut) {
+ link = await this.topSiteToSearchTopSite(link);
+ } else if (siteData.sponsored_position) {
+ if (contileEnabled && hasContileTiles) {
+ continue;
+ }
+ const {
+ sponsored_position,
+ sponsored_tile_id,
+ sponsored_impression_url,
+ sponsored_click_url,
+ } = siteData;
+ link = {
+ sponsored_position,
+ sponsored_tile_id,
+ sponsored_impression_url,
+ sponsored_click_url,
+ show_sponsored_label: link.hostname !== "yandex",
+ ...link,
+ };
+ }
+ DEFAULT_TOP_SITES.push(link);
+ }
+
+ this.refresh({ broadcast: true, isStartup });
+ }
+
+ refreshDefaults(sites, { isStartup = false } = {}) {
+ // Clear out the array of any previous defaults
+ DEFAULT_TOP_SITES.length = 0;
+
+ // Add default sites if any based on the pref
+ if (sites) {
+ for (const url of sites.split(",")) {
+ const site = {
+ isDefault: true,
+ url,
+ };
+ site.hostname = shortURL(site);
+ DEFAULT_TOP_SITES.push(site);
+ }
+ }
+
+ this.refresh({ broadcast: true, isStartup });
+ }
+
+ async _getRemoteConfig(firstTime = true) {
+ if (!this._remoteConfig) {
+ this._remoteConfig = await lazy.RemoteSettings("top-sites");
+ this._remoteConfig.on("sync", () => {
+ this._readDefaults();
+ });
+ }
+
+ let result = [];
+ let failed = false;
+ try {
+ result = await this._remoteConfig.get();
+ } catch (ex) {
+ console.error(ex);
+ failed = true;
+ }
+ if (!result.length) {
+ console.error("Received empty top sites configuration!");
+ failed = true;
+ }
+ // If we failed, or the result is empty, try loading from the local dump.
+ if (firstTime && failed) {
+ await this._remoteConfig.db.clear();
+ // Now call this again.
+ return this._getRemoteConfig(false);
+ }
+
+ // Sort sites based on the "order" attribute.
+ result.sort((a, b) => a.order - b.order);
+
+ result = result.filter(topsite => {
+ // Filter by region.
+ if (topsite.exclude_regions?.includes(lazy.Region.home)) {
+ return false;
+ }
+ if (
+ topsite.include_regions?.length &&
+ !topsite.include_regions.includes(lazy.Region.home)
+ ) {
+ return false;
+ }
+
+ // Filter by locale.
+ if (topsite.exclude_locales?.includes(Services.locale.appLocaleAsBCP47)) {
+ return false;
+ }
+ if (
+ topsite.include_locales?.length &&
+ !topsite.include_locales.includes(Services.locale.appLocaleAsBCP47)
+ ) {
+ return false;
+ }
+
+ // Filter by experiment.
+ // Exclude this top site if any of the specified experiments are running.
+ if (
+ topsite.exclude_experiments?.some(experimentID =>
+ Services.prefs.getBoolPref(
+ DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH + experimentID,
+ false
+ )
+ )
+ ) {
+ return false;
+ }
+ // Exclude this top site if none of the specified experiments are running.
+ if (
+ topsite.include_experiments?.length &&
+ topsite.include_experiments.every(
+ experimentID =>
+ !Services.prefs.getBoolPref(
+ DEFAULT_SITES_EXPERIMENTS_PREF_BRANCH + experimentID,
+ false
+ )
+ )
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+
+ return result;
+ }
+
+ filterForThumbnailExpiration(callback) {
+ const { rows } = this.store.getState().TopSites;
+ callback(
+ rows.reduce((acc, site) => {
+ acc.push(site.url);
+ if (site.customScreenshotURL) {
+ acc.push(site.customScreenshotURL);
+ }
+ return acc;
+ }, [])
+ );
+ }
+
+ /**
+ * shouldFilterSearchTile - is default filtering enabled and does a given hostname match the user's default search engine?
+ *
+ * @param {string} hostname a top site hostname, such as "amazon" or "foo"
+ * @returns {bool}
+ */
+ shouldFilterSearchTile(hostname) {
+ if (
+ this.store.getState().Prefs.values[FILTER_DEFAULT_SEARCH_PREF] &&
+ (SEARCH_FILTERS.includes(hostname) ||
+ hostname === this._currentSearchHostname)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * _maybeInsertSearchShortcuts - if the search shortcuts experiment is running,
+ * insert search shortcuts if needed
+ *
+ * @param {Array} plainPinnedSites (from the pinnedSitesCache)
+ * @returns {boolean} Did we insert any search shortcuts?
+ */
+ async _maybeInsertSearchShortcuts(plainPinnedSites) {
+ // Only insert shortcuts if the experiment is running
+ if (this.store.getState().Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT]) {
+ // We don't want to insert shortcuts we've previously inserted
+ const prevInsertedShortcuts = this.store
+ .getState()
+ .Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF].split(",")
+ .filter(s => s); // Filter out empty strings
+ const newInsertedShortcuts = [];
+
+ let shouldPin = this._useRemoteSetting
+ ? DEFAULT_TOP_SITES.filter(s => s.searchTopSite).map(s => s.hostname)
+ : this.store
+ .getState()
+ .Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF].split(",");
+ shouldPin = shouldPin
+ .map(getSearchProvider)
+ .filter(s => s && s.shortURL !== this._currentSearchHostname);
+
+ // If we've previously inserted all search shortcuts return early
+ if (
+ shouldPin.every(shortcut =>
+ prevInsertedShortcuts.includes(shortcut.shortURL)
+ )
+ ) {
+ return false;
+ }
+
+ const numberOfSlots =
+ this.store.getState().Prefs.values[ROWS_PREF] *
+ TOP_SITES_MAX_SITES_PER_ROW;
+
+ // The plainPinnedSites array is populated with pinned sites at their
+ // respective indices, and null everywhere else, but is not always the
+ // right length
+ const emptySlots = Math.max(numberOfSlots - plainPinnedSites.length, 0);
+ const pinnedSites = [...plainPinnedSites].concat(
+ Array(emptySlots).fill(null)
+ );
+
+ const tryToInsertSearchShortcut = async shortcut => {
+ const nextAvailable = pinnedSites.indexOf(null);
+ // Only add a search shortcut if the site isn't already pinned, we
+ // haven't previously inserted it, there's space to pin it, and the
+ // search engine is available in Firefox
+ if (
+ !pinnedSites.find(s => s && shortURL(s) === shortcut.shortURL) &&
+ !prevInsertedShortcuts.includes(shortcut.shortURL) &&
+ nextAvailable > -1 &&
+ (await checkHasSearchEngine(shortcut.keyword))
+ ) {
+ const site = await this.topSiteToSearchTopSite({ url: shortcut.url });
+ this._pinSiteAt(site, nextAvailable);
+ pinnedSites[nextAvailable] = site;
+ newInsertedShortcuts.push(shortcut.shortURL);
+ }
+ };
+
+ for (let shortcut of shouldPin) {
+ await tryToInsertSearchShortcut(shortcut);
+ }
+
+ if (newInsertedShortcuts.length) {
+ this.store.dispatch(
+ ac.SetPref(
+ SEARCH_SHORTCUTS_HAVE_PINNED_PREF,
+ prevInsertedShortcuts.concat(newInsertedShortcuts).join(",")
+ )
+ );
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Fetch topsites spocs from the DiscoveryStream feed.
+ *
+ * @returns {Array} An array of sponsored tile objects.
+ */
+ fetchDiscoveryStreamSpocs() {
+ let sponsored = [];
+ const { DiscoveryStream } = this.store.getState();
+ if (DiscoveryStream) {
+ const discoveryStreamSpocs =
+ DiscoveryStream.spocs.data["sponsored-topsites"]?.items || [];
+ // Find the first component of a type and remove it from layout
+ const findSponsoredTopsitesPositions = name => {
+ for (const row of DiscoveryStream.layout) {
+ for (const component of row.components) {
+ if (component.placement?.name === name) {
+ return component.spocs.positions;
+ }
+ }
+ }
+ return null;
+ };
+
+ // Get positions from layout for now. This could be improved if we store position data in state.
+ const discoveryStreamSpocPositions =
+ findSponsoredTopsitesPositions("sponsored-topsites");
+
+ if (discoveryStreamSpocPositions?.length) {
+ function reformatImageURL(url, width, height) {
+ // Change the image URL to request a size tailored for the parent container width
+ // Also: force JPEG, quality 60, no upscaling, no EXIF data
+ // Uses Thumbor: https://thumbor.readthedocs.io/en/latest/usage.html
+ // For now we wrap this in single quotes because this is being used in a url() css rule, and otherwise would cause a parsing error.
+ return `'https://img-getpocket.cdn.mozilla.net/${width}x${height}/filters:format(jpeg):quality(60):no_upscale():strip_exif()/${encodeURIComponent(
+ url
+ )}'`;
+ }
+
+ // We need to loop through potential spocs and set their positions.
+ // If we run out of spocs or positions, we stop.
+ // First, we need to know which array is shortest. This is our exit condition.
+ const minLength = Math.min(
+ discoveryStreamSpocPositions.length,
+ discoveryStreamSpocs.length
+ );
+ // Loop until we run out of spocs or positions.
+ for (let i = 0; i < minLength; i++) {
+ const positionIndex = discoveryStreamSpocPositions[i].index;
+ const spoc = discoveryStreamSpocs[i];
+ const link = {
+ favicon: reformatImageURL(spoc.raw_image_src, 96, 96),
+ faviconSize: 96,
+ type: "SPOC",
+ label: spoc.title || spoc.sponsor,
+ title: spoc.title || spoc.sponsor,
+ url: spoc.url,
+ flightId: spoc.flight_id,
+ id: spoc.id,
+ guid: spoc.id,
+ shim: spoc.shim,
+ // For now we are assuming position based on intended position.
+ // Actual position can shift based on other content.
+ // We send the intended position in the ping.
+ pos: positionIndex,
+ // Set this so that SPOC topsites won't be shown in the URL bar.
+ // See Bug 1822027. Note that `sponsored_position` is 1-based.
+ sponsored_position: positionIndex + 1,
+ // This is used for topsites deduping.
+ hostname: shortURL({ url: spoc.url }),
+ partner: SPONSORED_TILE_PARTNER_MOZ_SALES,
+ };
+ sponsored.push(link);
+ }
+ }
+ }
+ return sponsored;
+ }
+
+ // eslint-disable-next-line max-statements
+ async getLinksWithDefaults(isStartup = false) {
+ const prefValues = this.store.getState().Prefs.values;
+ const numItems = prefValues[ROWS_PREF] * TOP_SITES_MAX_SITES_PER_ROW;
+ const searchShortcutsExperiment = prefValues[SEARCH_SHORTCUTS_EXPERIMENT];
+ // We must wait for search services to initialize in order to access default
+ // search engine properties without triggering a synchronous initialization
+ try {
+ await Services.search.init();
+ } catch {
+ // We continue anyway because we want the user to see their sponsored,
+ // saved, or visited shortcut tiles even if search engines are not
+ // available.
+ }
+
+ // Get all frecent sites from history.
+ let frecent = [];
+ let cache;
+ try {
+ // Request can throw if executing the linkGetter inside LinksCache returns
+ // a null object.
+ cache = await this.frecentCache.request({
+ // We need to overquery due to the top 5 alexa search + default search possibly being removed
+ numItems: numItems + SEARCH_FILTERS.length + 1,
+ topsiteFrecency: FRECENCY_THRESHOLD,
+ });
+ } catch (ex) {
+ cache = [];
+ }
+
+ for (let link of cache) {
+ // The cache can contain null values.
+ if (!link) {
+ continue;
+ }
+ const hostname = shortURL(link);
+ if (!this.shouldFilterSearchTile(hostname)) {
+ frecent.push({
+ ...(searchShortcutsExperiment
+ ? await this.topSiteToSearchTopSite(link)
+ : link),
+ hostname,
+ });
+ }
+ }
+
+ // Get defaults.
+ let contileSponsored = [];
+ let notBlockedDefaultSites = [];
+ for (let link of DEFAULT_TOP_SITES) {
+ // For sponsored Yandex links, default filtering is reversed: we only
+ // show them if Yandex is the default search engine.
+ if (link.sponsored_position && link.hostname === "yandex") {
+ if (link.hostname !== this._currentSearchHostname) {
+ continue;
+ }
+ } else if (this.shouldFilterSearchTile(link.hostname)) {
+ continue;
+ }
+ // Drop blocked default sites.
+ if (
+ lazy.NewTabUtils.blockedLinks.isBlocked({
+ url: link.url,
+ })
+ ) {
+ continue;
+ }
+ // If we've previously blocked a search shortcut, remove the default top site
+ // that matches the hostname
+ const searchProvider = getSearchProvider(shortURL(link));
+ if (
+ searchProvider &&
+ lazy.NewTabUtils.blockedLinks.isBlocked({ url: searchProvider.url })
+ ) {
+ continue;
+ }
+ if (link.sponsored_position) {
+ if (!prefValues[SHOW_SPONSORED_PREF]) {
+ continue;
+ }
+ contileSponsored[link.sponsored_position - 1] = link;
+
+ // Unpin search shortcut if present for the sponsored link to be shown
+ // instead.
+ this._unpinSearchShortcut(link.hostname);
+ } else {
+ notBlockedDefaultSites.push(
+ searchShortcutsExperiment
+ ? await this.topSiteToSearchTopSite(link)
+ : link
+ );
+ }
+ }
+ this._telemetryUtility.determineFilteredTilesAndSetToDismissed(
+ contileSponsored
+ );
+
+ const discoverySponsored = this.fetchDiscoveryStreamSpocs();
+ this._telemetryUtility.setTiles(discoverySponsored);
+
+ const sponsored = this._mergeSponsoredLinks({
+ [SPONSORED_TILE_PARTNER_AMP]: contileSponsored,
+ [SPONSORED_TILE_PARTNER_MOZ_SALES]: discoverySponsored,
+ });
+
+ this._maybeCapSponsoredLinks(sponsored);
+
+ // This will set all extra tiles to oversold, including moz-sales.
+ this._telemetryUtility.determineFilteredTilesAndSetToOversold(sponsored);
+
+ // Get pinned links augmented with desired properties
+ let plainPinned = await this.pinnedCache.request();
+
+ // Insert search shortcuts if we need to.
+ // _maybeInsertSearchShortcuts returns true if any search shortcuts are
+ // inserted, meaning we need to expire and refresh the pinnedCache
+ if (await this._maybeInsertSearchShortcuts(plainPinned)) {
+ this.pinnedCache.expire();
+ plainPinned = await this.pinnedCache.request();
+ }
+
+ const pinned = await Promise.all(
+ plainPinned.map(async link => {
+ if (!link) {
+ return link;
+ }
+
+ // Drop pinned search shortcuts when their engine has been removed / hidden.
+ if (link.searchTopSite) {
+ const searchProvider = getSearchProvider(shortURL(link));
+ if (
+ !searchProvider ||
+ !(await checkHasSearchEngine(searchProvider.keyword))
+ ) {
+ return null;
+ }
+ }
+
+ // Copy all properties from a frecent link and add more
+ const finder = other => other.url === link.url;
+
+ // Remove frecent link's screenshot if pinned link has a custom one
+ const frecentSite = frecent.find(finder);
+ if (frecentSite && link.customScreenshotURL) {
+ delete frecentSite.screenshot;
+ }
+ // If the link is a frecent site, do not copy over 'isDefault', else check
+ // if the site is a default site
+ const copy = Object.assign(
+ {},
+ frecentSite || { isDefault: !!notBlockedDefaultSites.find(finder) },
+ link,
+ { hostname: shortURL(link) },
+ { searchTopSite: !!link.searchTopSite }
+ );
+
+ // Add in favicons if we don't already have it
+ if (!copy.favicon) {
+ try {
+ lazy.NewTabUtils.activityStreamProvider._faviconBytesToDataURI(
+ await lazy.NewTabUtils.activityStreamProvider._addFavicons([copy])
+ );
+
+ for (const prop of PINNED_FAVICON_PROPS_TO_MIGRATE) {
+ copy.__sharedCache.updateLink(prop, copy[prop]);
+ }
+ } catch (e) {
+ // Some issue with favicon, so just continue without one
+ }
+ }
+
+ return copy;
+ })
+ );
+
+ // Remove any duplicates from frecent and default sites
+ const [, dedupedSponsored, dedupedFrecent, dedupedDefaults] =
+ this.dedupe.group(pinned, sponsored, frecent, notBlockedDefaultSites);
+ const dedupedUnpinned = [...dedupedFrecent, ...dedupedDefaults];
+
+ // Remove adult sites if we need to
+ const checkedAdult = lazy.FilterAdult.filter(dedupedUnpinned);
+
+ // Insert the original pinned sites into the deduped frecent and defaults.
+ let withPinned = insertPinned(checkedAdult, pinned);
+ // Insert sponsored sites at their desired position.
+ dedupedSponsored.forEach(link => {
+ if (!link) {
+ return;
+ }
+ let index = link.sponsored_position - 1;
+ if (index >= withPinned.length) {
+ withPinned[index] = link;
+ } else if (withPinned[index]?.sponsored_position) {
+ // We currently want DiscoveryStream spocs to replace existing spocs.
+ withPinned[index] = link;
+ } else {
+ withPinned.splice(index, 0, link);
+ }
+ });
+ // Remove excess items after we inserted sponsored ones.
+ withPinned = withPinned.slice(0, numItems);
+
+ // Now, get a tippy top icon, a rich icon, or screenshot for every item
+ for (const link of withPinned) {
+ if (link) {
+ // If there is a custom screenshot this is the only image we display
+ if (link.customScreenshotURL) {
+ this._fetchScreenshot(link, link.customScreenshotURL, isStartup);
+ } else if (link.searchTopSite && !link.isDefault) {
+ await this._attachTippyTopIconForSearchShortcut(link, link.label);
+ } else {
+ this._fetchIcon(link, isStartup);
+ }
+
+ // Remove internal properties that might be updated after dispatch
+ delete link.__sharedCache;
+
+ // Indicate that these links should get a frecency bonus when clicked
+ link.typedBonus = true;
+ }
+ }
+
+ this._linksWithDefaults = withPinned;
+
+ this._telemetryUtility.finalizeNewtabPingFields(dedupedSponsored);
+ return withPinned;
+ }
+
+ /**
+ * Cap sponsored links if they're more than the specified maximum.
+ *
+ * @param {Array} links An array of sponsored links. Capping will be performed in-place.
+ */
+ _maybeCapSponsoredLinks(links) {
+ // Set maximum sponsored top sites
+ const maxSponsored =
+ lazy.NimbusFeatures.pocketNewtab.getVariable(
+ NIMBUS_VARIABLE_MAX_SPONSORED
+ ) ?? MAX_NUM_SPONSORED;
+ if (links.length > maxSponsored) {
+ links.length = maxSponsored;
+ }
+ }
+
+ /**
+ * Merge sponsored links from all the partners using SOV if present.
+ * For each tile position, the user is assigned to one partner via stable sampling.
+ * If the chosen partner doesn't have a tile to serve, another tile from a different
+ * partner is used as the replacement.
+ *
+ * @param {object} sponsoredLinks An object with sponsored links from all the partners.
+ * @returns {Array} An array of merged sponsored links.
+ */
+ _mergeSponsoredLinks(sponsoredLinks) {
+ const { positions: allocatedPositions, ready: sovReady } =
+ this.store.getState().TopSites.sov || {};
+ if (
+ !this._contile.sov ||
+ !sovReady ||
+ !lazy.NimbusFeatures.pocketNewtab.getVariable(
+ NIMBUS_VARIABLE_CONTILE_SOV_ENABLED
+ )
+ ) {
+ return Object.values(sponsoredLinks).flat();
+ }
+
+ // AMP links might have empty slots, remove them as SOV doesn't need those.
+ sponsoredLinks[SPONSORED_TILE_PARTNER_AMP] =
+ sponsoredLinks[SPONSORED_TILE_PARTNER_AMP].filter(Boolean);
+
+ let sponsored = [];
+ let chosenPartners = [];
+
+ for (const allocation of allocatedPositions) {
+ let link = null;
+ const { assignedPartner } = allocation;
+ if (assignedPartner) {
+ // Unknown partners are allowed so that new parters can be added to Shepherd
+ // sooner without waiting for client changes.
+ link = sponsoredLinks[assignedPartner]?.shift();
+ }
+
+ if (!link) {
+ // If the chosen partner doesn't have a tile for this postion, choose any
+ // one from another group. For simplicity, we do _not_ do resampling here
+ // against the remaining partners.
+ for (const partner of SPONSORED_TILE_PARTNERS) {
+ if (
+ partner === assignedPartner ||
+ sponsoredLinks[partner].length === 0
+ ) {
+ continue;
+ }
+ link = sponsoredLinks[partner].shift();
+ break;
+ }
+
+ if (!link) {
+ // No more links to be added across all the partners, just return.
+ if (chosenPartners.length) {
+ Glean.newtab.sovAllocation.set(
+ chosenPartners.map(entry => JSON.stringify(entry))
+ );
+ }
+ return sponsored;
+ }
+ }
+
+ // Update the position fields. Note that postion is also 1-based in SOV.
+ link.sponsored_position = allocation.position;
+ if (link.pos !== undefined) {
+ // Pocket `pos` is 0-based.
+ link.pos = allocation.position - 1;
+ }
+ sponsored.push(link);
+
+ chosenPartners.push({
+ pos: allocation.position,
+ assigned: assignedPartner, // The assigned partner based on SOV
+ chosen: link.partner,
+ });
+ }
+ // Record chosen partners to glean
+ if (chosenPartners.length) {
+ Glean.newtab.sovAllocation.set(
+ chosenPartners.map(entry => JSON.stringify(entry))
+ );
+ }
+
+ // add the remaining contile sponsoredLinks when nimbus variable present
+ if (
+ lazy.NimbusFeatures.pocketNewtab.getVariable(
+ NIMBUS_VARIABLE_CONTILE_MAX_NUM_SPONSORED
+ )
+ ) {
+ return sponsored.concat(sponsoredLinks[SPONSORED_TILE_PARTNER_AMP]);
+ }
+
+ return sponsored;
+ }
+
+ /**
+ * Attach TippyTop icon to the given search shortcut
+ *
+ * Note that it queries the search form URL from search service For Yandex,
+ * and uses it to choose the best icon for its shortcut variants.
+ *
+ * @param {object} link A link object with a `url` property
+ * @param {string} keyword Search keyword
+ */
+ async _attachTippyTopIconForSearchShortcut(link, keyword) {
+ if (
+ ["@\u044F\u043D\u0434\u0435\u043A\u0441", "@yandex"].includes(keyword)
+ ) {
+ let site = { url: link.url };
+ site.url = (await getSearchFormURL(keyword)) || site.url;
+ this._tippyTopProvider.processSite(site);
+ link.tippyTopIcon = site.tippyTopIcon;
+ link.smallFavicon = site.smallFavicon;
+ link.backgroundColor = site.backgroundColor;
+ } else {
+ this._tippyTopProvider.processSite(link);
+ }
+ }
+
+ /**
+ * Refresh the top sites data for content.
+ *
+ * @param {object} options
+ * @param {bool} options.broadcast Should the update be broadcasted.
+ * @param {bool} options.isStartup Being called while TopSitesFeed is initting.
+ */
+ async refresh(options = {}) {
+ // Avoiding refreshing if it's already happening.
+ if (this._refreshing) {
+ return;
+ }
+ if (!this._startedUp && !options.isStartup) {
+ // Initial refresh still pending.
+ return;
+ }
+ this._refreshing = true;
+ this._startedUp = true;
+
+ if (!this._tippyTopProvider.initialized) {
+ await this._tippyTopProvider.init();
+ }
+
+ const links = await this.getLinksWithDefaults({
+ isStartup: options.isStartup,
+ });
+ const newAction = { type: at.TOP_SITES_UPDATED, data: { links } };
+ let storedPrefs;
+ try {
+ storedPrefs = (await this._storage.get(SECTION_ID)) || {};
+ } catch (e) {
+ storedPrefs = {};
+ console.error("Problem getting stored prefs for TopSites");
+ }
+ newAction.data.pref = getDefaultOptions(storedPrefs);
+
+ if (options.isStartup) {
+ newAction.meta = {
+ isStartup: true,
+ };
+ }
+
+ if (options.broadcast) {
+ // Broadcast an update to all open content pages
+ this.store.dispatch(ac.BroadcastToContent(newAction));
+ } else {
+ // Don't broadcast only update the state and update the preloaded tab.
+ this.store.dispatch(ac.AlsoToPreloaded(newAction));
+ }
+ this._refreshing = false;
+ if (Cu.isInAutomation) {
+ Services.obs.notifyObservers(null, "topsites-refreshed");
+ }
+ }
+
+ // Allocate ad positions to partners based on SOV via stable randomization.
+ async allocatePositions() {
+ // If the fetch to get sov fails for whatever reason, we can just return here.
+ // Code that uses this falls back to flattening allocations instead if this has failed.
+ if (!this._contile.sov) {
+ return;
+ }
+ // This sample input should ensure we return the same result for this allocation,
+ // even if called from other parts of the code.
+ const sampleInput = `${lazy.contextId}-${this._contile.sov.name}`;
+ const allocatedPositions = [];
+ for (const allocation of this._contile.sov.allocations) {
+ const allocatedPosition = {
+ position: allocation.position,
+ };
+ allocatedPositions.push(allocatedPosition);
+ const ratios = allocation.allocation.map(alloc => alloc.percentage);
+ if (ratios.length) {
+ const index = await lazy.Sampling.ratioSample(sampleInput, ratios);
+ allocatedPosition.assignedPartner =
+ allocation.allocation[index].partner;
+ }
+ }
+
+ this.store.dispatch(
+ ac.OnlyToMain({
+ type: at.SOV_UPDATED,
+ data: {
+ ready: !!allocatedPositions.length,
+ positions: allocatedPositions,
+ },
+ })
+ );
+ }
+
+ async updateCustomSearchShortcuts(isStartup = false) {
+ if (!this.store.getState().Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT]) {
+ return;
+ }
+
+ if (!this._tippyTopProvider.initialized) {
+ await this._tippyTopProvider.init();
+ }
+
+ // Populate the state with available search shortcuts
+ let searchShortcuts = [];
+ for (const engine of await Services.search.getAppProvidedEngines()) {
+ const shortcut = CUSTOM_SEARCH_SHORTCUTS.find(s =>
+ engine.aliases.includes(s.keyword)
+ );
+ if (shortcut) {
+ let clone = { ...shortcut };
+ await this._attachTippyTopIconForSearchShortcut(clone, clone.keyword);
+ searchShortcuts.push(clone);
+ }
+ }
+
+ this.store.dispatch(
+ ac.BroadcastToContent({
+ type: at.UPDATE_SEARCH_SHORTCUTS,
+ data: { searchShortcuts },
+ meta: {
+ isStartup,
+ },
+ })
+ );
+ }
+
+ async topSiteToSearchTopSite(site) {
+ const searchProvider = getSearchProvider(shortURL(site));
+ if (
+ !searchProvider ||
+ !(await checkHasSearchEngine(searchProvider.keyword))
+ ) {
+ return site;
+ }
+ return {
+ ...site,
+ searchTopSite: true,
+ label: searchProvider.keyword,
+ };
+ }
+
+ /**
+ * Get an image for the link preferring tippy top, rich favicon, screenshots.
+ */
+ async _fetchIcon(link, isStartup = false) {
+ // Nothing to do if we already have a rich icon from the page
+ if (link.favicon && link.faviconSize >= MIN_FAVICON_SIZE) {
+ return;
+ }
+
+ // Nothing more to do if we can use a default tippy top icon
+ this._tippyTopProvider.processSite(link);
+ if (link.tippyTopIcon) {
+ return;
+ }
+
+ // Make a request for a better icon
+ this._requestRichIcon(link.url);
+
+ // Also request a screenshot if we don't have one yet
+ await this._fetchScreenshot(link, link.url, isStartup);
+ }
+
+ /**
+ * Fetch, cache and broadcast a screenshot for a specific topsite.
+ *
+ * @param {object} link cached topsite object
+ * @param {string} url where to fetch the image from
+ * @param {boolean} isStartup Whether the screenshot is fetched while
+ * initting TopSitesFeed.
+ */
+ async _fetchScreenshot(link, url, isStartup = false) {
+ // We shouldn't bother caching screenshots if they won't be shown.
+ if (
+ link.screenshot ||
+ !this.store.getState().Prefs.values[SHOWN_ON_NEWTAB_PREF]
+ ) {
+ return;
+ }
+ await lazy.Screenshots.maybeCacheScreenshot(
+ link,
+ url,
+ "screenshot",
+ screenshot =>
+ this.store.dispatch(
+ ac.BroadcastToContent({
+ data: { screenshot, url: link.url },
+ type: at.SCREENSHOT_UPDATED,
+ meta: {
+ isStartup,
+ },
+ })
+ )
+ );
+ }
+
+ /**
+ * Dispatch screenshot preview to target or notify if request failed.
+ *
+ * @param {string} url The URL used to capture the screenshot
+ * @param {string} target Id of content process where to dispatch the result
+ */
+ async getScreenshotPreview(url, target) {
+ const preview = (await lazy.Screenshots.getScreenshotForURL(url)) || "";
+ this.store.dispatch(
+ ac.OnlyToOneContent(
+ {
+ data: { url, preview },
+ type: at.PREVIEW_RESPONSE,
+ },
+ target
+ )
+ );
+ }
+
+ _requestRichIcon(url) {
+ this.store.dispatch({
+ type: at.RICH_ICON_MISSING,
+ data: { url },
+ });
+ }
+
+ updateSectionPrefs(collapsed) {
+ this.store.dispatch(
+ ac.BroadcastToContent({
+ type: at.TOP_SITES_PREFS_UPDATED,
+ data: { pref: collapsed },
+ })
+ );
+ }
+
+ /**
+ * Inform others that top sites data has been updated due to pinned changes.
+ */
+ _broadcastPinnedSitesUpdated() {
+ // Pinned data changed, so make sure we get latest
+ this.pinnedCache.expire();
+
+ // Refresh to update pinned sites with screenshots, trigger deduping, etc.
+ this.refresh({ broadcast: true });
+ }
+
+ /**
+ * Pin a site at a specific position saving only the desired keys.
+ *
+ * @param customScreenshotURL {string} User set URL of preview image for site
+ * @param label {string} User set string of custom site name
+ */
+ // To refactor in Bug 1891997
+ /* eslint-enable jsdoc/check-param-names */
+ async _pinSiteAt({ customScreenshotURL, label, url, searchTopSite }, index) {
+ const toPin = { url };
+ if (label) {
+ toPin.label = label;
+ }
+ if (customScreenshotURL) {
+ toPin.customScreenshotURL = customScreenshotURL;
+ }
+ if (searchTopSite) {
+ toPin.searchTopSite = searchTopSite;
+ }
+ lazy.NewTabUtils.pinnedLinks.pin(toPin, index);
+
+ await this._clearLinkCustomScreenshot({ customScreenshotURL, url });
+ }
+
+ async _clearLinkCustomScreenshot(site) {
+ // If screenshot url changed or was removed we need to update the cached link obj
+ if (site.customScreenshotURL !== undefined) {
+ const pinned = await this.pinnedCache.request();
+ const link = pinned.find(pin => pin && pin.url === site.url);
+ if (link && link.customScreenshotURL !== site.customScreenshotURL) {
+ link.__sharedCache.updateLink("screenshot", undefined);
+ }
+ }
+ }
+
+ /**
+ * Handle a pin action of a site to a position.
+ */
+ async pin(action) {
+ let { site, index } = action.data;
+ index = this._adjustPinIndexForSponsoredLinks(site, index);
+ // If valid index provided, pin at that position
+ if (index >= 0) {
+ await this._pinSiteAt(site, index);
+ this._broadcastPinnedSitesUpdated();
+ } else {
+ // Bug 1458658. If the top site is being pinned from an 'Add a Top Site' option,
+ // then we want to make sure to unblock that link if it has previously been
+ // blocked. We know if the site has been added because the index will be -1.
+ if (index === -1) {
+ lazy.NewTabUtils.blockedLinks.unblock({ url: site.url });
+ this.frecentCache.expire();
+ }
+ this.insert(action);
+ }
+ }
+
+ /**
+ * Handle an unpin action of a site.
+ */
+ unpin(action) {
+ const { site } = action.data;
+ lazy.NewTabUtils.pinnedLinks.unpin(site);
+ this._broadcastPinnedSitesUpdated();
+ }
+
+ unpinAllSearchShortcuts() {
+ Services.prefs.clearUserPref(
+ `browser.newtabpage.activity-stream.${SEARCH_SHORTCUTS_HAVE_PINNED_PREF}`
+ );
+ for (let pinnedLink of lazy.NewTabUtils.pinnedLinks.links) {
+ if (pinnedLink && pinnedLink.searchTopSite) {
+ lazy.NewTabUtils.pinnedLinks.unpin(pinnedLink);
+ }
+ }
+ this.pinnedCache.expire();
+ }
+
+ _unpinSearchShortcut(vendor) {
+ for (let pinnedLink of lazy.NewTabUtils.pinnedLinks.links) {
+ if (
+ pinnedLink &&
+ pinnedLink.searchTopSite &&
+ shortURL(pinnedLink) === vendor
+ ) {
+ lazy.NewTabUtils.pinnedLinks.unpin(pinnedLink);
+ this.pinnedCache.expire();
+
+ const prevInsertedShortcuts = this.store
+ .getState()
+ .Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF].split(",");
+ this.store.dispatch(
+ ac.SetPref(
+ SEARCH_SHORTCUTS_HAVE_PINNED_PREF,
+ prevInsertedShortcuts.filter(s => s !== vendor).join(",")
+ )
+ );
+ break;
+ }
+ }
+ }
+
+ /**
+ * Reduces the given pinning index by the number of preceding sponsored
+ * sites, to accomodate for sponsored sites pushing pinned ones to the side,
+ * effectively increasing their index again.
+ */
+ _adjustPinIndexForSponsoredLinks(site, index) {
+ if (!this._linksWithDefaults) {
+ return index;
+ }
+ // Adjust insertion index for sponsored sites since their position is
+ // fixed.
+ let adjustedIndex = index;
+ for (let i = 0; i < index; i++) {
+ const link = this._linksWithDefaults[i];
+ if (
+ link &&
+ link.sponsored_position &&
+ this._linksWithDefaults[i]?.url !== site.url
+ ) {
+ adjustedIndex--;
+ }
+ }
+ return adjustedIndex;
+ }
+
+ /**
+ * Insert a site to pin at a position shifting over any other pinned sites.
+ */
+ _insertPin(site, originalIndex, draggedFromIndex) {
+ let index = this._adjustPinIndexForSponsoredLinks(site, originalIndex);
+
+ // Don't insert any pins past the end of the visible top sites. Otherwise,
+ // we can end up with a bunch of pinned sites that can never be unpinned again
+ // from the UI.
+ const topSitesCount =
+ this.store.getState().Prefs.values[ROWS_PREF] *
+ TOP_SITES_MAX_SITES_PER_ROW;
+ if (index >= topSitesCount) {
+ return;
+ }
+
+ let pinned = lazy.NewTabUtils.pinnedLinks.links;
+ if (!pinned[index]) {
+ this._pinSiteAt(site, index);
+ } else {
+ pinned[draggedFromIndex] = null;
+ // Find the hole to shift the pinned site(s) towards. We shift towards the
+ // hole left by the site being dragged.
+ let holeIndex = index;
+ const indexStep = index > draggedFromIndex ? -1 : 1;
+ while (pinned[holeIndex]) {
+ holeIndex += indexStep;
+ }
+ if (holeIndex >= topSitesCount || holeIndex < 0) {
+ // There are no holes, so we will effectively unpin the last slot and shifting
+ // towards it. This only happens when adding a new top site to an already
+ // fully pinned grid.
+ holeIndex = topSitesCount - 1;
+ }
+
+ // Shift towards the hole.
+ const shiftingStep = holeIndex > index ? -1 : 1;
+ while (holeIndex !== index) {
+ const nextIndex = holeIndex + shiftingStep;
+ this._pinSiteAt(pinned[nextIndex], holeIndex);
+ holeIndex = nextIndex;
+ }
+ this._pinSiteAt(site, index);
+ }
+ }
+
+ /**
+ * Handle an insert (drop/add) action of a site.
+ */
+ async insert(action) {
+ let { index } = action.data;
+ // Treat invalid pin index values (e.g., -1, undefined) as insert in the first position
+ if (!(index > 0)) {
+ index = 0;
+ }
+
+ // Inserting a top site pins it in the specified slot, pushing over any link already
+ // pinned in the slot (unless it's the last slot, then it replaces).
+ this._insertPin(
+ action.data.site,
+ index,
+ action.data.draggedFromIndex !== undefined
+ ? action.data.draggedFromIndex
+ : this.store.getState().Prefs.values[ROWS_PREF] *
+ TOP_SITES_MAX_SITES_PER_ROW
+ );
+
+ await this._clearLinkCustomScreenshot(action.data.site);
+ this._broadcastPinnedSitesUpdated();
+ }
+
+ updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts }) {
+ // Unpin the deletedShortcuts.
+ deletedShortcuts.forEach(({ url }) => {
+ lazy.NewTabUtils.pinnedLinks.unpin({ url });
+ });
+
+ // Pin the addedShortcuts.
+ const numberOfSlots =
+ this.store.getState().Prefs.values[ROWS_PREF] *
+ TOP_SITES_MAX_SITES_PER_ROW;
+ addedShortcuts.forEach(shortcut => {
+ // Find first hole in pinnedLinks.
+ let index = lazy.NewTabUtils.pinnedLinks.links.findIndex(link => !link);
+ if (
+ index < 0 &&
+ lazy.NewTabUtils.pinnedLinks.links.length + 1 < numberOfSlots
+ ) {
+ // pinnedLinks can have less slots than the total available.
+ index = lazy.NewTabUtils.pinnedLinks.links.length;
+ }
+ if (index >= 0) {
+ lazy.NewTabUtils.pinnedLinks.pin(shortcut, index);
+ } else {
+ // No slots available, we need to do an insert in first slot and push over other pinned links.
+ this._insertPin(shortcut, 0, numberOfSlots);
+ }
+ });
+
+ this._broadcastPinnedSitesUpdated();
+ }
+
+ onAction(action) {
+ switch (action.type) {
+ case at.INIT:
+ this.init();
+ this.updateCustomSearchShortcuts(true /* isStartup */);
+ break;
+ case at.SYSTEM_TICK:
+ this.refresh({ broadcast: false });
+ this._contile.periodicUpdate();
+ break;
+ // All these actions mean we need new top sites
+ case at.PLACES_HISTORY_CLEARED:
+ case at.PLACES_LINKS_DELETED:
+ this.frecentCache.expire();
+ this.refresh({ broadcast: true });
+ break;
+ case at.PLACES_LINKS_CHANGED:
+ this.frecentCache.expire();
+ this.refresh({ broadcast: false });
+ break;
+ case at.PLACES_LINK_BLOCKED:
+ this.frecentCache.expire();
+ this.pinnedCache.expire();
+ this.refresh({ broadcast: true });
+ break;
+ case at.PREF_CHANGED:
+ switch (action.data.name) {
+ case DEFAULT_SITES_PREF:
+ if (!this._useRemoteSetting) {
+ this.refreshDefaults(action.data.value);
+ }
+ break;
+ case ROWS_PREF:
+ case FILTER_DEFAULT_SEARCH_PREF:
+ case SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF:
+ this.refresh({ broadcast: true });
+ break;
+ case SHOW_SPONSORED_PREF:
+ if (
+ lazy.NimbusFeatures.newtab.getVariable(
+ NIMBUS_VARIABLE_CONTILE_ENABLED
+ )
+ ) {
+ this._contile.refresh();
+ } else {
+ this.refresh({ broadcast: true });
+ }
+ if (!action.data.value) {
+ this._contile._resetContileCachePrefs();
+ }
+
+ break;
+ case SEARCH_SHORTCUTS_EXPERIMENT:
+ if (action.data.value) {
+ this.updateCustomSearchShortcuts();
+ } else {
+ this.unpinAllSearchShortcuts();
+ }
+ this.refresh({ broadcast: true });
+ }
+ break;
+ case at.UPDATE_SECTION_PREFS:
+ if (action.data.id === SECTION_ID) {
+ this.updateSectionPrefs(action.data.value);
+ }
+ break;
+ case at.PREFS_INITIAL_VALUES:
+ if (!this._useRemoteSetting) {
+ this.refreshDefaults(action.data[DEFAULT_SITES_PREF]);
+ }
+ break;
+ case at.TOP_SITES_PIN:
+ this.pin(action);
+ break;
+ case at.TOP_SITES_UNPIN:
+ this.unpin(action);
+ break;
+ case at.TOP_SITES_INSERT:
+ this.insert(action);
+ break;
+ case at.PREVIEW_REQUEST:
+ this.getScreenshotPreview(action.data.url, action.meta.fromTarget);
+ break;
+ case at.UPDATE_PINNED_SEARCH_SHORTCUTS:
+ this.updatePinnedSearchShortcuts(action.data);
+ break;
+ case at.DISCOVERY_STREAM_SPOCS_UPDATE:
+ // Refresh to update sponsored topsites.
+ this.refresh({ broadcast: true, isStartup: action.meta.isStartup });
+ break;
+ case at.UNINIT:
+ this.uninit();
+ break;
+ }
+ }
+}
+
+export const TopSites = new _TopSites();
diff --git a/browser/components/topsites/moz.build b/browser/components/topsites/moz.build
new file mode 100644
index 0000000000..f8c7a96fa2
--- /dev/null
+++ b/browser/components/topsites/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += ["TopSites.sys.mjs"]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Top Sites")
diff --git a/browser/components/topsites/test/unit/test_top_sites.js b/browser/components/topsites/test/unit/test_top_sites.js
new file mode 100644
index 0000000000..3de5f43262
--- /dev/null
+++ b/browser/components/topsites/test/unit/test_top_sites.js
@@ -0,0 +1,3571 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TopSites, DEFAULT_TOP_SITES } = ChromeUtils.importESModule(
+ "resource:///modules/TopSites.sys.mjs"
+);
+
+const { actionCreators: ac, actionTypes: at } = ChromeUtils.importESModule(
+ "resource://activity-stream/common/Actions.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs",
+ NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
+ shortURL: "resource://activity-stream/lib/ShortURL.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+ Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs",
+ Sampling: "resource://gre/modules/components-utils/Sampling.sys.mjs",
+ SearchService: "resource://gre/modules/SearchService.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+ TOP_SITES_DEFAULT_ROWS: "resource://activity-stream/common/Reducers.sys.mjs",
+ TOP_SITES_MAX_SITES_PER_ROW:
+ "resource://activity-stream/common/Reducers.sys.mjs",
+});
+
+const FAKE_FAVICON = "data987";
+const FAKE_FAVICON_SIZE = 128;
+const FAKE_FRECENCY = 200;
+const FAKE_LINKS = new Array(2 * TOP_SITES_MAX_SITES_PER_ROW)
+ .fill(null)
+ .map((v, i) => ({
+ frecency: FAKE_FRECENCY,
+ url: `http://www.site${i}.com`,
+ }));
+const FAKE_SCREENSHOT = "data123";
+const SEARCH_SHORTCUTS_EXPERIMENT_PREF = "improvesearch.topSiteSearchShortcuts";
+const SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF =
+ "improvesearch.topSiteSearchShortcuts.searchEngines";
+const SEARCH_SHORTCUTS_HAVE_PINNED_PREF =
+ "improvesearch.topSiteSearchShortcuts.havePinned";
+const SHOWN_ON_NEWTAB_PREF = "feeds.topsites";
+const SHOW_SPONSORED_PREF = "showSponsoredTopSites";
+const TOP_SITES_BLOCKED_SPONSORS_PREF = "browser.topsites.blockedSponsors";
+const CONTILE_CACHE_PREF = "browser.topsites.contile.cachedTiles";
+
+// This pref controls how long the contile cache is valid for in seconds.
+const CONTILE_CACHE_VALID_FOR_SECONDS_PREF =
+ "browser.topsites.contile.cacheValidFor";
+// This pref records when the last contile fetch occurred, as a UNIX timestamp
+// in seconds.
+const CONTILE_CACHE_LAST_FETCH_PREF = "browser.topsites.contile.lastFetch";
+
+function FakeTippyTopProvider() {}
+FakeTippyTopProvider.prototype = {
+ async init() {
+ this.initialized = true;
+ },
+ processSite(site) {
+ return site;
+ },
+};
+
+let gSearchServiceInitStub;
+let gGetTopSitesStub;
+
+function stubTopSites(sandbox) {
+ let cachedStorage = TopSites._storage;
+ let cachedStore = TopSites.store;
+
+ async function cleanup() {
+ if (TopSites._refreshing) {
+ info("Wait for refresh to finish.");
+ // Wait for refresh to finish or else removing the store while a process
+ // is running will result in errors.
+ await TestUtils.topicObserved("topsites-refreshed");
+ }
+ TopSites._tippyTopProvider.initialized = false;
+ TopSites._storage = cachedStorage;
+ TopSites.store = cachedStore;
+ TopSites.pinnedCache.clear();
+ TopSites.frecentCache.clear();
+ info("Finished cleaning up TopSites.");
+ }
+
+ const storage = {
+ init: sandbox.stub().resolves(),
+ get: sandbox.stub().resolves(),
+ set: sandbox.stub().resolves(),
+ };
+
+ // Setup for tests that don't call `init` but require feed.storage
+ TopSites._storage = storage;
+ TopSites.store = {
+ dispatch: sinon.spy(),
+ getState() {
+ return this.state;
+ },
+ state: {
+ Prefs: { values: { topSitesRows: 2 } },
+ TopSites: { rows: Array(12).fill("site") },
+ },
+ dbStorage: { getDbTable: sandbox.stub().returns(storage) },
+ };
+ info("Created mock store for TopSites.");
+ return cleanup;
+}
+
+add_setup(async () => {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(SearchService.prototype, "defaultEngine").get(() => {
+ return { identifier: "ddg", searchForm: "https://duckduckgo.com" };
+ });
+
+ gGetTopSitesStub = sandbox
+ .stub(NewTabUtils.activityStreamLinks, "getTopSites")
+ .resolves(FAKE_LINKS);
+
+ gSearchServiceInitStub = sandbox
+ .stub(SearchService.prototype, "init")
+ .resolves();
+
+ sandbox.stub(NewTabUtils.activityStreamProvider, "_faviconBytesToDataURI");
+
+ sandbox
+ .stub(NewTabUtils.activityStreamProvider, "_addFavicons")
+ .callsFake(l => {
+ return Promise.resolve(
+ l.map(link => {
+ link.favicon = FAKE_FAVICON;
+ link.faviconSize = FAKE_FAVICON_SIZE;
+ return link;
+ })
+ );
+ });
+
+ sandbox.stub(Screenshots, "getScreenshotForURL").resolves(FAKE_SCREENSHOT);
+ sandbox.spy(Screenshots, "maybeCacheScreenshot");
+ sandbox.stub(Screenshots, "_shouldGetScreenshots").returns(true);
+
+ registerCleanupFunction(() => {
+ sandbox.restore();
+ });
+});
+
+add_task(async function test_construction() {
+ Assert.ok(TopSites._currentSearchHostname, "_currentSearchHostname defined");
+});
+
+add_task(async function test_refreshDefaults() {
+ let sandbox = sinon.createSandbox();
+ let cleanup = stubTopSites(sandbox);
+ Assert.ok(
+ !DEFAULT_TOP_SITES.length,
+ "Should have 0 DEFAULT_TOP_SITES initially."
+ );
+
+ info("refreshDefaults should add defaults on PREFS_INITIAL_VALUES");
+ TopSites.onAction({
+ type: at.PREFS_INITIAL_VALUES,
+ data: { "default.sites": "https://foo.com" },
+ });
+
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 1,
+ "Should have 1 DEFAULT_TOP_SITES now."
+ );
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should add defaults on default.sites PREF_CHANGED");
+ TopSites.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: "default.sites", value: "https://foo.com" },
+ });
+
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 1,
+ "Should have 1 DEFAULT_TOP_SITES now."
+ );
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should refresh on topSiteRows PREF_CHANGED");
+ let refreshStub = sandbox.stub(TopSites, "refresh");
+ TopSites.onAction({ type: at.PREF_CHANGED, data: { name: "topSitesRows" } });
+ Assert.ok(TopSites.refresh.calledOnce, "refresh called");
+ refreshStub.restore();
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should have default sites with .isDefault = true");
+ TopSites.refreshDefaults("https://foo.com");
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 1,
+ "Should have a DEFAULT_TOP_SITES now."
+ );
+ Assert.ok(
+ DEFAULT_TOP_SITES[0].isDefault,
+ "Lone top site should be the default."
+ );
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should have default sites with appropriate hostname");
+ TopSites.refreshDefaults("https://foo.com");
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 1,
+ "Should have a DEFAULT_TOP_SITES now."
+ );
+ let [site] = DEFAULT_TOP_SITES;
+ Assert.equal(
+ site.hostname,
+ shortURL(site),
+ "Lone top site should have the right hostname."
+ );
+
+ // Reset the DEFAULT_TOP_SITES;
+ DEFAULT_TOP_SITES.length = 0;
+
+ info("refreshDefaults should add no defaults on empty pref");
+ TopSites.refreshDefaults("");
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 0,
+ "Should have 0 DEFAULT_TOP_SITES now."
+ );
+
+ info("refreshDefaults should be able to clear defaults");
+ TopSites.refreshDefaults("https://foo.com");
+ TopSites.refreshDefaults("");
+
+ Assert.equal(
+ DEFAULT_TOP_SITES.length,
+ 0,
+ "Should have 0 DEFAULT_TOP_SITES now."
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_filterForThumbnailExpiration() {
+ let sandbox = sinon.createSandbox();
+ let cleanup = stubTopSites(sandbox);
+
+ info(
+ "filterForThumbnailExpiration should pass rows.urls to the callback provided"
+ );
+ const rows = [
+ { url: "foo.com" },
+ { url: "bar.com", customScreenshotURL: "custom" },
+ ];
+ TopSites.store.state.TopSites = { rows };
+ const stub = sandbox.stub();
+ TopSites.filterForThumbnailExpiration(stub);
+ Assert.ok(stub.calledOnce);
+ Assert.ok(stub.calledWithExactly(["foo.com", "bar.com", "custom"]));
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_on_SearchService_init_failure() {
+ let sandbox = sinon.createSandbox();
+ let cleanup = stubTopSites(sandbox);
+
+ TopSites.refreshDefaults("https://foo.com");
+
+ gSearchServiceInitStub.rejects(
+ new Error("Simulating search init failures")
+ );
+
+ const result = await TopSites.getLinksWithDefaults();
+ Assert.ok(result);
+
+ gSearchServiceInitStub.resolves();
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults() {
+ NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
+
+ let sandbox = sinon.createSandbox();
+ let cleanup = stubTopSites(sandbox);
+
+ TopSites.refreshDefaults("https://foo.com");
+
+ info("getLinksWithDefaults should get the links from NewTabUtils");
+ let result = await TopSites.getLinksWithDefaults();
+
+ const reference = FAKE_LINKS.map(site =>
+ Object.assign({}, site, {
+ hostname: shortURL(site),
+ typedBonus: true,
+ })
+ );
+
+ Assert.deepEqual(result, reference);
+ Assert.ok(NewTabUtils.activityStreamLinks.getTopSites.calledOnce);
+
+ info("getLinksWithDefaults should indicate the links get typed bonus");
+ Assert.ok(result[0].typedBonus, "Expected typed bonus property to be true.");
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_filterAdult() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should filter out non-pinned adult sites");
+
+ sandbox.stub(FilterAdult, "filter").returns([]);
+ const TEST_URL = "https://foo.com/";
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [{ url: TEST_URL }]);
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ const result = await TopSites.getLinksWithDefaults();
+ Assert.ok(FilterAdult.filter.calledOnce);
+ Assert.equal(result.length, 1);
+ Assert.equal(result[0].url, TEST_URL);
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_caching() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "getLinksWithDefaults should filter out the defaults that have been blocked"
+ );
+ // make sure we only have one top site, and we block the only default site we have to show
+ const url = "www.myonlytopsite.com";
+ const topsite = {
+ frecency: FAKE_FRECENCY,
+ hostname: shortURL({ url }),
+ typedBonus: true,
+ url,
+ };
+
+ const blockedDefaultSite = { url: "https://foo.com" };
+ gGetTopSitesStub.resolves([topsite]);
+ sandbox.stub(NewTabUtils.blockedLinks, "isBlocked").callsFake(site => {
+ return site.url === blockedDefaultSite.url;
+ });
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+ const result = await TopSites.getLinksWithDefaults();
+
+ // what we should be left with is just the top site we added, and not the default site we blocked
+ Assert.equal(result.length, 1);
+ Assert.deepEqual(result[0], topsite);
+ let foundBlocked = result.find(site => site.url === blockedDefaultSite.url);
+ Assert.ok(!foundBlocked, "Should not have found blocked site.");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_dedupe() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should call dedupe.group on the links");
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let stub = sandbox.stub(TopSites.dedupe, "group").callsFake((...id) => id);
+ await TopSites.getLinksWithDefaults();
+
+ Assert.ok(stub.calledOnce, "dedupe.group was called once");
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test__dedupe_key() {
+ let sandbox = sinon.createSandbox();
+
+ info("_dedupeKey should dedupe on hostname instead of url");
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let site = { url: "foo", hostname: "bar" };
+ let result = TopSites._dedupeKey(site);
+
+ Assert.equal(result, site.hostname, "deduped on hostname");
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_adds_defaults() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "getLinksWithDefaults should add defaults if there are are not enough links"
+ );
+ const TEST_LINKS = [{ frecency: FAKE_FRECENCY, url: "foo.com" }];
+ gGetTopSitesStub.resolves(TEST_LINKS);
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ let reference = [...TEST_LINKS, ...DEFAULT_TOP_SITES].map(s =>
+ Object.assign({}, s, {
+ hostname: shortURL(s),
+ typedBonus: true,
+ })
+ );
+
+ Assert.deepEqual(result, reference);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_adds_defaults_for_visible_slots() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "getLinksWithDefaults should only add defaults up to the number of visible slots"
+ );
+ const numVisible = TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;
+ let testLinks = [];
+ for (let i = 0; i < numVisible - 1; i++) {
+ testLinks.push({ frecency: FAKE_FRECENCY, url: `foo${i}.com` });
+ }
+ gGetTopSitesStub.resolves(testLinks);
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ let reference = [...testLinks, DEFAULT_TOP_SITES[0]].map(s =>
+ Object.assign({}, s, {
+ hostname: shortURL(s),
+ typedBonus: true,
+ })
+ );
+
+ Assert.equal(result.length, numVisible);
+ Assert.deepEqual(result, reference);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_no_throw_on_no_links() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should not throw if NewTabUtils returns null");
+ gGetTopSitesStub.resolves(null);
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ await TopSites.getLinksWithDefaults();
+ Assert.ok(true, "getLinksWithDefaults did not throw");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_get_more_on_request() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should get more if the user has asked for more");
+ let testLinks = new Array(4 * TOP_SITES_MAX_SITES_PER_ROW)
+ .fill(null)
+ .map((v, i) => ({
+ frecency: FAKE_FRECENCY,
+ url: `http://www.site${i}.com`,
+ }));
+ gGetTopSitesStub.resolves(testLinks);
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ const TEST_ROWS = 3;
+ TopSites.store.state.Prefs.values.topSitesRows = TEST_ROWS;
+
+ let result = await TopSites.getLinksWithDefaults();
+ Assert.equal(result.length, TEST_ROWS * TOP_SITES_MAX_SITES_PER_ROW);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_reuse_cache() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should reuse the cache on subsequent calls");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ await TopSites.getLinksWithDefaults();
+ await TopSites.getLinksWithDefaults();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
+ "getTopSites only called once"
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_ignore_cache_on_requesting_more() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should ignore the cache when requesting more");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ await TopSites.getLinksWithDefaults();
+ TopSites.store.state.Prefs.values.topSitesRows *= 3;
+ await TopSites.getLinksWithDefaults();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledTwice,
+ "getTopSites called twice"
+ );
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(
+ async function test_getLinksWithDefaults_migrate_frecent_screenshot_data() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should migrate frecent screenshot data without getting screenshots again"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ TopSites.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
+ await TopSites.getLinksWithDefaults();
+
+ let originalCallCount = Screenshots.getScreenshotForURL.callCount;
+ TopSites.frecentCache.expire();
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledTwice,
+ "getTopSites called twice"
+ );
+ Assert.equal(
+ Screenshots.getScreenshotForURL.callCount,
+ originalCallCount,
+ "getScreenshotForURL was not called again."
+ );
+ Assert.equal(result[0].screenshot, FAKE_SCREENSHOT);
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(
+ async function test_getLinksWithDefaults_migrate_pinned_favicon_data() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should migrate pinned favicon data without getting favicons again"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resetHistory();
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ await TopSites.getLinksWithDefaults();
+
+ let originalCallCount =
+ NewTabUtils.activityStreamProvider._addFavicons.callCount;
+ TopSites.pinnedCache.expire();
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ Assert.equal(
+ NewTabUtils.activityStreamProvider._addFavicons.callCount,
+ originalCallCount,
+ "_addFavicons was not called again."
+ );
+ Assert.equal(result[0].favicon, FAKE_FAVICON);
+ Assert.equal(result[0].faviconSize, FAKE_FAVICON_SIZE);
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_no_internal_properties() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not expose internal link properties");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ let internal = Object.keys(result[0]).filter(key => key.startsWith("__"));
+ Assert.equal(internal.join(""), "");
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_copy_frecent_screenshot() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should copy the screenshot of the frecent site if " +
+ "pinned site doesn't have customScreenshotURL"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ const TEST_SCREENSHOT = "screenshot";
+
+ gGetTopSitesStub.resolves([
+ { url: "https://foo.com/", screenshot: TEST_SCREENSHOT },
+ ]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, TEST_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_no_copy_frecent_screenshot() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should not copy the frecent screenshot if " +
+ "customScreenshotURL is set"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ gGetTopSitesStub.resolves([
+ { url: "https://foo.com/", screenshot: "screenshot" },
+ ]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/", customScreenshotURL: "custom" }]);
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, undefined);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_persist_screenshot() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should keep the same screenshot if no frecent site is found"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ const CUSTOM_SCREENSHOT = "custom";
+
+ gGetTopSitesStub.resolves([]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/", screenshot: CUSTOM_SCREENSHOT }]);
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, CUSTOM_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_no_overwrite_pinned_screenshot() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not overwrite pinned site screenshot");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ const EXISTING_SCREENSHOT = "some-screenshot";
+
+ gGetTopSitesStub.resolves([{ url: "https://foo.com/", screenshot: "foo" }]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ { url: "https://foo.com/", screenshot: EXISTING_SCREENSHOT },
+ ]);
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ Assert.equal(result[0].screenshot, EXISTING_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(
+ async function test_getLinksWithDefaults_no_searchTopSite_from_frecent() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not set searchTopSite from frecent site");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ const EXISTING_SCREENSHOT = "some-screenshot";
+
+ gGetTopSitesStub.resolves([
+ {
+ url: "https://foo.com/",
+ searchTopSite: true,
+ screenshot: EXISTING_SCREENSHOT,
+ },
+ ]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ let result = await TopSites.getLinksWithDefaults();
+
+ Assert.ok(!result[0].searchTopSite);
+ // But it should copy over other properties
+ Assert.equal(result[0].screenshot, EXISTING_SCREENSHOT);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_concurrency_getTopSites() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults concurrent calls should call the backing data once"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
+
+ await Promise.all([
+ TopSites.getLinksWithDefaults(),
+ TopSites.getLinksWithDefaults(),
+ ]);
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
+ "getTopSites only called once"
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_getLinksWithDefaults_concurrency_getScreenshotForURL() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults concurrent calls should call the backing data once"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+ TopSites.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
+
+ NewTabUtils.activityStreamLinks.getTopSites.resetHistory();
+ Screenshots.getScreenshotForURL.resetHistory();
+
+ await Promise.all([
+ TopSites.getLinksWithDefaults(),
+ TopSites.getLinksWithDefaults(),
+ ]);
+
+ Assert.ok(
+ NewTabUtils.activityStreamLinks.getTopSites.calledOnce,
+ "getTopSites only called once"
+ );
+
+ Assert.equal(
+ Screenshots.getScreenshotForURL.callCount,
+ FAKE_LINKS.length,
+ "getLinksWithDefaults concurrent calls should get screenshots once per link"
+ );
+ await cleanup();
+
+ cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[SHOWN_ON_NEWTAB_PREF] = true;
+
+ TopSites.refreshDefaults("https://foo.com");
+
+ sandbox.stub(TopSites, "_requestRichIcon");
+ await Promise.all([
+ TopSites.getLinksWithDefaults(),
+ TopSites.getLinksWithDefaults(),
+ ]);
+
+ Assert.equal(
+ TopSites.store.dispatch.callCount,
+ FAKE_LINKS.length,
+ "getLinksWithDefaults concurrent calls should dispatch once per link screenshot fetched"
+ );
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(async function test_getLinksWithDefaults_deduping_no_dedupe_pinned() {
+ let sandbox = sinon.createSandbox();
+ info("getLinksWithDefaults should not dedupe pinned sites");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults("https://foo.com");
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ { url: "https://developer.mozilla.org/en-US/docs/Web" },
+ { url: "https://developer.mozilla.org/en-US/docs/Learn" },
+ ]);
+
+ let sites = await TopSites.getLinksWithDefaults();
+ Assert.equal(sites.length, 2 * TOP_SITES_MAX_SITES_PER_ROW);
+ Assert.equal(sites[0].url, NewTabUtils.pinnedLinks.links[0].url);
+ Assert.equal(sites[1].url, NewTabUtils.pinnedLinks.links[1].url);
+ Assert.equal(sites[0].hostname, sites[1].hostname);
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_prefer_pinned_sites() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should prefer pinned sites over links");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults();
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ { url: "https://developer.mozilla.org/en-US/docs/Web" },
+ { url: "https://developer.mozilla.org/en-US/docs/Learn" },
+ ]);
+
+ const SECOND_TOP_SITE_URL = "https://www.mozilla.org/";
+
+ gGetTopSitesStub.resolves([
+ { frecency: FAKE_FRECENCY, url: "https://developer.mozilla.org/" },
+ { frecency: FAKE_FRECENCY, url: SECOND_TOP_SITE_URL },
+ ]);
+
+ let sites = await TopSites.getLinksWithDefaults();
+
+ // Expecting 3 links where there's 2 pinned and 1 www.mozilla.org, so
+ // the frecent with matching hostname as pinned is removed.
+ Assert.equal(sites.length, 3);
+ Assert.equal(sites[0].url, NewTabUtils.pinnedLinks.links[0].url);
+ Assert.equal(sites[1].url, NewTabUtils.pinnedLinks.links[1].url);
+ Assert.equal(sites[2].url, SECOND_TOP_SITE_URL);
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_title_and_null() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should return sites that have a title");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults();
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://github.com/mozilla/activity-stream" }]);
+
+ let sites = await TopSites.getLinksWithDefaults();
+ for (let site of sites) {
+ Assert.ok(site.hostname);
+ }
+
+ info("getLinksWithDefaults should not throw for null entries");
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [null]);
+ await TopSites.getLinksWithDefaults();
+ Assert.ok(true, "getLinksWithDefaults didn't throw");
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_calls__fetchIcon() {
+ let sandbox = sinon.createSandbox();
+
+ info("getLinksWithDefaults should return sites that have a title");
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults();
+
+ sandbox.spy(TopSites, "_fetchIcon");
+ let results = await TopSites.getLinksWithDefaults();
+ Assert.ok(results.length, "Got back some results");
+ Assert.equal(TopSites._fetchIcon.callCount, results.length);
+ for (let result of results) {
+ Assert.ok(TopSites._fetchIcon.calledWith(result));
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_calls__fetchScreenshot() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "getLinksWithDefaults should call _fetchScreenshot when customScreenshotURL is set"
+ );
+
+ gGetTopSitesStub.resolves([]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com", customScreenshotURL: "custom" }]);
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults();
+
+ sandbox.stub(TopSites, "_fetchScreenshot");
+ await TopSites.getLinksWithDefaults();
+
+ Assert.ok(TopSites._fetchScreenshot.calledWith(sinon.match.object, "custom"));
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getLinksWithDefaults_with_DiscoveryStream() {
+ let sandbox = sinon.createSandbox();
+ info(
+ "getLinksWithDefaults should add a sponsored topsite from discoverystream to all the valid indices"
+ );
+
+ let makeStreamData = index => ({
+ layout: [
+ {
+ components: [
+ {
+ placement: {
+ name: "sponsored-topsites",
+ },
+ spocs: {
+ positions: [{ index }],
+ },
+ },
+ ],
+ },
+ ],
+ spocs: {
+ data: {
+ "sponsored-topsites": {
+ items: [{ title: "test spoc", url: "https://test-spoc.com" }],
+ },
+ },
+ },
+ });
+
+ let cleanup = stubTopSites(sandbox);
+ TopSites.refreshDefaults();
+
+ for (let i = 0; i < FAKE_LINKS.length; i++) {
+ TopSites.store.state.DiscoveryStream = makeStreamData(i);
+ const result = await TopSites.getLinksWithDefaults();
+ const link = result[i];
+
+ Assert.equal(link.type, "SPOC");
+ Assert.equal(link.title, "test spoc");
+ Assert.equal(link.sponsored_position, i + 1);
+ Assert.equal(link.hostname, "test-spoc");
+ Assert.equal(link.url, "https://test-spoc.com");
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_init() {
+ let sandbox = sinon.createSandbox();
+
+ sandbox.stub(NimbusFeatures.newtab, "onUpdate");
+
+ let cleanup = stubTopSites(sandbox);
+
+ sandbox.stub(TopSites, "refresh");
+ await TopSites.init();
+
+ info("TopSites.init should call refresh (broadcast: true)");
+ Assert.ok(TopSites.refresh.calledOnce, "refresh called once");
+ Assert.ok(
+ TopSites.refresh.calledWithExactly({
+ broadcast: true,
+ isStartup: true,
+ })
+ );
+
+ info("TopSites.init should initialise the storage");
+ Assert.ok(
+ TopSites.store.dbStorage.getDbTable.calledOnce,
+ "getDbTable called once"
+ );
+ Assert.ok(
+ TopSites.store.dbStorage.getDbTable.calledWithExactly("sectionPrefs")
+ );
+
+ info("TopSites.init should call onUpdate to set up Nimbus update listener");
+
+ Assert.ok(
+ NimbusFeatures.newtab.onUpdate.calledOnce,
+ "NimbusFeatures.newtab.onUpdate called once"
+ );
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh() {
+ let sandbox = sinon.createSandbox();
+
+ sandbox.stub(NimbusFeatures.newtab, "onUpdate");
+
+ let cleanup = stubTopSites(sandbox);
+
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ info("TopSites.refresh should wait for tippytop to initialize");
+ TopSites._tippyTopProvider.initialized = false;
+ sandbox.stub(TopSites._tippyTopProvider, "init").resolves();
+
+ await TopSites.refresh();
+
+ Assert.ok(
+ TopSites._tippyTopProvider.init.calledOnce,
+ "TopSites._tippyTopProvider.init called once"
+ );
+
+ info(
+ "TopSites.refresh should not init the tippyTopProvider if already initialized"
+ );
+ TopSites._tippyTopProvider.initialized = true;
+ TopSites._tippyTopProvider.init.resetHistory();
+
+ await TopSites.refresh();
+
+ Assert.ok(
+ TopSites._tippyTopProvider.init.notCalled,
+ "tippyTopProvider not initted again"
+ );
+
+ info("TopSites.refresh should broadcast TOP_SITES_UPDATED");
+ TopSites.store.dispatch.resetHistory();
+ sandbox.stub(TopSites, "getLinksWithDefaults").resolves([]);
+
+ await TopSites.refresh({ broadcast: true });
+
+ Assert.ok(TopSites.store.dispatch.calledOnce, "dispatch called once");
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.BroadcastToContent({
+ type: at.TOP_SITES_UPDATED,
+ data: { links: [], pref: { collapsed: false } },
+ })
+ )
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_dispatch() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.refresh should dispatch an action with the links returned");
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ await TopSites.refresh({ broadcast: true });
+ let reference = FAKE_LINKS.map(site =>
+ Object.assign({}, site, {
+ hostname: shortURL(site),
+ typedBonus: true,
+ })
+ );
+
+ Assert.ok(TopSites.store.dispatch.calledOnce, "Store.dispatch called once");
+ Assert.equal(
+ TopSites.store.dispatch.firstCall.args[0].type,
+ at.TOP_SITES_UPDATED
+ );
+ Assert.deepEqual(
+ TopSites.store.dispatch.firstCall.args[0].data.links,
+ reference
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_empty_slots() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.refresh should handle empty slots in the resulting top sites array"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ gGetTopSitesStub.resolves([FAKE_LINKS[0]]);
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ null,
+ null,
+ FAKE_LINKS[1],
+ null,
+ null,
+ null,
+ null,
+ null,
+ FAKE_LINKS[2],
+ ]);
+
+ await TopSites.refresh({ broadcast: true });
+
+ Assert.ok(TopSites.store.dispatch.calledOnce, "Store.dispatch called once");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_to_preloaded() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.refresh should dispatch AlsoToPreloaded when broadcast is false"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ gGetTopSitesStub.resolves([]);
+ await TopSites.refresh({ broadcast: false });
+
+ Assert.ok(TopSites.store.dispatch.calledOnce, "Store.dispatch called once");
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.AlsoToPreloaded({
+ type: at.TOP_SITES_UPDATED,
+ data: { links: [], pref: { collapsed: false } },
+ })
+ )
+ );
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_init_storage() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.refresh should not init storage of it's already initialized");
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ TopSites._storage.initialized = true;
+
+ await TopSites.refresh({ broadcast: false });
+
+ Assert.ok(
+ TopSites._storage.init.notCalled,
+ "TopSites._storage.init was not called."
+ );
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_refresh_handles_indexedDB_errors() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.refresh should dispatch AlsoToPreloaded when broadcast is false"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "_fetchIcon");
+ TopSites._startedUp = true;
+
+ TopSites._storage.get.throws(new Error());
+
+ try {
+ await TopSites.refresh({ broadcast: false });
+ Assert.ok(true, "refresh should have succeeded");
+ } catch (e) {
+ Assert.ok(false, "Should not have thrown");
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_updateSectionPrefs_on_UPDATE_SECTION_PREFS() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.onAction should call updateSectionPrefs on UPDATE_SECTION_PREFS"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "updateSectionPrefs");
+ TopSites.onAction({
+ type: at.UPDATE_SECTION_PREFS,
+ data: { id: "topsites" },
+ });
+
+ Assert.ok(
+ TopSites.updateSectionPrefs.calledOnce,
+ "TopSites.updateSectionPrefs called once"
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(
+ async function test_updateSectionPrefs_dispatch_TOP_SITES_PREFS_UPDATED() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.updateSectionPrefs should dispatch TOP_SITES_PREFS_UPDATED");
+
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.updateSectionPrefs({ collapsed: true });
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.BroadcastToContent({
+ type: at.TOP_SITES_PREFS_UPDATED,
+ data: { pref: { collapsed: true } },
+ })
+ )
+ );
+
+ sandbox.restore();
+ await cleanup();
+ }
+);
+
+add_task(async function test_allocatePositions() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.allocationPositions should allocate positions and dispatch");
+
+ let cleanup = stubTopSites(sandbox);
+
+ let sov = {
+ name: "SOV-20230518215316",
+ allocations: [
+ {
+ position: 1,
+ allocation: [
+ {
+ partner: "amp",
+ percentage: 100,
+ },
+ {
+ partner: "moz-sales",
+ percentage: 0,
+ },
+ ],
+ },
+ {
+ position: 2,
+ allocation: [
+ {
+ partner: "amp",
+ percentage: 80,
+ },
+ {
+ partner: "moz-sales",
+ percentage: 20,
+ },
+ ],
+ },
+ ],
+ };
+
+ sandbox.stub(TopSites._contile, "sov").get(() => sov);
+
+ sandbox.stub(Sampling, "ratioSample");
+ Sampling.ratioSample.onCall(0).resolves(0);
+ Sampling.ratioSample.onCall(1).resolves(1);
+
+ await TopSites.allocatePositions();
+
+ Assert.ok(
+ TopSites.store.dispatch.calledOnce,
+ "TopSites.store.dispatch called once"
+ );
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.OnlyToMain({
+ type: at.SOV_UPDATED,
+ data: {
+ ready: true,
+ positions: [
+ { position: 1, assignedPartner: "amp" },
+ { position: 2, assignedPartner: "moz-sales" },
+ ],
+ },
+ })
+ )
+ );
+
+ Sampling.ratioSample.onCall(2).resolves(0);
+ Sampling.ratioSample.onCall(3).resolves(0);
+
+ await TopSites.allocatePositions();
+
+ Assert.ok(
+ TopSites.store.dispatch.calledTwice,
+ "TopSites.store.dispatch called twice"
+ );
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.OnlyToMain({
+ type: at.SOV_UPDATED,
+ data: {
+ ready: true,
+ positions: [
+ { position: 1, assignedPartner: "amp" },
+ { position: 2, assignedPartner: "amp" },
+ ],
+ },
+ })
+ )
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getScreenshotPreview() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.getScreenshotPreview should dispatch preview if request is succesful"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.getScreenshotPreview("custom", 1234);
+
+ Assert.ok(TopSites.store.dispatch.calledOnce);
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.OnlyToOneContent(
+ {
+ data: { preview: FAKE_SCREENSHOT, url: "custom" },
+ type: at.PREVIEW_RESPONSE,
+ },
+ 1234
+ )
+ )
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_getScreenshotPreview() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.getScreenshotPreview should return empty string if request fails"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ Screenshots.getScreenshotForURL.resolves(Promise.resolve(null));
+ await TopSites.getScreenshotPreview("custom", 1234);
+
+ Assert.ok(TopSites.store.dispatch.calledOnce);
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.OnlyToOneContent(
+ {
+ data: { preview: "", url: "custom" },
+ type: at.PREVIEW_RESPONSE,
+ },
+ 1234
+ )
+ )
+ );
+
+ Screenshots.getScreenshotForURL.resolves(FAKE_SCREENSHOT);
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_onAction_part_1() {
+ let sandbox = sinon.createSandbox();
+
+ info("TopSites.onAction should call getScreenshotPreview on PREVIEW_REQUEST");
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "getScreenshotPreview");
+ TopSites.onAction({
+ type: at.PREVIEW_REQUEST,
+ data: { url: "foo" },
+ meta: { fromTarget: 1234 },
+ });
+
+ Assert.ok(
+ TopSites.getScreenshotPreview.calledOnce,
+ "TopSites.getScreenshotPreview called once"
+ );
+ Assert.ok(TopSites.getScreenshotPreview.calledWithExactly("foo", 1234));
+
+ info("TopSites.onAction should refresh on SYSTEM_TICK");
+ sandbox.stub(TopSites, "refresh");
+ TopSites.onAction({ type: at.SYSTEM_TICK });
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: false }));
+
+ info(
+ "TopSites.onAction should call with correct parameters on TOP_SITES_PIN"
+ );
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.spy(TopSites, "pin");
+
+ let pinAction = {
+ type: at.TOP_SITES_PIN,
+ data: { site: { url: "foo.com" }, index: 7 },
+ };
+ TopSites.onAction(pinAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledWithExactly(
+ pinAction.data.site,
+ pinAction.data.index
+ )
+ );
+ Assert.ok(
+ TopSites.pin.calledOnce,
+ "TopSites.onAction should call pin on TOP_SITES_PIN"
+ );
+
+ info(
+ "TopSites.onAction should unblock a previously blocked top site if " +
+ "we are now adding it manually via 'Add a Top Site' option"
+ );
+ sandbox.stub(NewTabUtils.blockedLinks, "unblock");
+ pinAction = {
+ type: at.TOP_SITES_PIN,
+ data: { site: { url: "foo.com" }, index: -1 },
+ };
+ TopSites.onAction(pinAction);
+ Assert.ok(
+ NewTabUtils.blockedLinks.unblock.calledWith({
+ url: pinAction.data.site.url,
+ })
+ );
+
+ info("TopSites.onAction should call insert on TOP_SITES_INSERT");
+ sandbox.stub(TopSites, "insert");
+ let addAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.com" } },
+ };
+
+ TopSites.onAction(addAction);
+ Assert.ok(TopSites.insert.calledOnce, "TopSites.insert called once");
+
+ info(
+ "TopSites.onAction should call unpin with correct parameters " +
+ "on TOP_SITES_UNPIN"
+ );
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [
+ null,
+ null,
+ { url: "foo.com" },
+ null,
+ null,
+ null,
+ null,
+ null,
+ FAKE_LINKS[0],
+ ]);
+ sandbox.stub(NewTabUtils.pinnedLinks, "unpin");
+
+ let unpinAction = {
+ type: at.TOP_SITES_UNPIN,
+ data: { site: { url: "foo.com" } },
+ };
+ TopSites.onAction(unpinAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.calledOnce,
+ "NewTabUtils.pinnedLinks.unpin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.unpin.calledWith(unpinAction.data.site));
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_onAction_part_2() {
+ let sandbox = sinon.createSandbox();
+
+ info(
+ "TopSites.onAction should call refresh without a target if we clear " +
+ "history with PLACES_HISTORY_CLEARED"
+ );
+
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "refresh");
+ TopSites.onAction({ type: at.PLACES_HISTORY_CLEARED });
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: true }));
+
+ TopSites.refresh.resetHistory();
+
+ info(
+ "TopSites.onAction should call refresh without a target " +
+ "if we remove a Topsite from history"
+ );
+ TopSites.onAction({ type: at.PLACES_LINKS_DELETED });
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: true }));
+
+ info("TopSites.onAction should call init on INIT action");
+ TopSites.onAction({ type: at.PLACES_LINKS_DELETED });
+ sandbox.stub(TopSites, "init");
+ TopSites.onAction({ type: at.INIT });
+ Assert.ok(TopSites.init.calledOnce, "TopSites.init called once");
+
+ info("TopSites.onAction should call refresh on PLACES_LINK_BLOCKED action");
+ TopSites.refresh.resetHistory();
+ await TopSites.onAction({ type: at.PLACES_LINK_BLOCKED });
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: true }));
+
+ info("TopSites.onAction should call refresh on PLACES_LINKS_CHANGED action");
+ TopSites.refresh.resetHistory();
+ await TopSites.onAction({ type: at.PLACES_LINKS_CHANGED });
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ Assert.ok(TopSites.refresh.calledWithExactly({ broadcast: false }));
+
+ info(
+ "TopSites.onAction should call pin with correct args on " +
+ "TOP_SITES_INSERT without an index specified"
+ );
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ let addAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.bar", label: "foo" } },
+ };
+ TopSites.onAction(addAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(addAction.data.site, 0));
+
+ info(
+ "TopSites.onAction should call pin with correct args on " +
+ "TOP_SITES_INSERT"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ let dropAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.bar", label: "foo" }, index: 3 },
+ };
+ TopSites.onAction(dropAction);
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(dropAction.data.site, 3));
+
+ // TopSites.init needs to actually run in order to register the observers that'll
+ // be removed in the following UNINIT test, otherwise uninit will throw.
+ TopSites.init.restore();
+ TopSites.init();
+
+ info("TopSites.onAction should remove the expiration filter on UNINIT");
+ sandbox.stub(PageThumbs, "removeExpirationFilter");
+ TopSites.onAction({ type: "UNINIT" });
+ Assert.ok(
+ PageThumbs.removeExpirationFilter.calledOnce,
+ "PageThumbs.removeExpirationFilter called once"
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_onAction_part_3() {
+ let sandbox = sinon.createSandbox();
+
+ let cleanup = stubTopSites(sandbox);
+
+ info(
+ "TopSites.onAction should call updatePinnedSearchShortcuts " +
+ "on UPDATE_PINNED_SEARCH_SHORTCUTS action"
+ );
+ sandbox.stub(TopSites, "updatePinnedSearchShortcuts");
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ await TopSites.onAction({
+ type: at.UPDATE_PINNED_SEARCH_SHORTCUTS,
+ data: { addedShortcuts },
+ });
+ Assert.ok(
+ TopSites.updatePinnedSearchShortcuts.calledOnce,
+ "TopSites.updatePinnedSearchShortcuts called once"
+ );
+
+ info(
+ "TopSites.onAction should refresh from Contile on " +
+ "SHOW_SPONSORED_PREF if Contile is enabled"
+ );
+ sandbox.spy(TopSites._contile, "refresh");
+ let prefChangeAction = {
+ type: at.PREF_CHANGED,
+ data: { name: SHOW_SPONSORED_PREF },
+ };
+ sandbox.stub(NimbusFeatures.newtab, "getVariable").returns(true);
+ TopSites.onAction(prefChangeAction);
+
+ Assert.ok(
+ TopSites._contile.refresh.calledOnce,
+ "TopSites._contile.refresh called once"
+ );
+
+ info(
+ "TopSites.onAction should not refresh from Contile on " +
+ "SHOW_SPONSORED_PREF if Contile is disabled"
+ );
+ NimbusFeatures.newtab.getVariable.returns(false);
+ TopSites._contile.refresh.resetHistory();
+ TopSites.onAction(prefChangeAction);
+
+ Assert.ok(
+ !TopSites._contile.refresh.calledOnce,
+ "TopSites._contile.refresh never called"
+ );
+
+ info(
+ "TopSites.onAction should reset Contile cache prefs " +
+ "when SHOW_SPONSORED_PREF is false"
+ );
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, "[]");
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ Math.round(Date.now() / 1000)
+ );
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 15 * 60);
+ prefChangeAction = {
+ type: at.PREF_CHANGED,
+ data: { name: SHOW_SPONSORED_PREF, value: false },
+ };
+ NimbusFeatures.newtab.getVariable.returns(true);
+ TopSites._contile.refresh.resetHistory();
+
+ TopSites.onAction(prefChangeAction);
+ Assert.ok(!Services.prefs.prefHasUserValue(CONTILE_CACHE_PREF));
+ Assert.ok(!Services.prefs.prefHasUserValue(CONTILE_CACHE_LAST_FETCH_PREF));
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(CONTILE_CACHE_VALID_FOR_SECONDS_PREF)
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_insert_part_1() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ let cleanup = stubTopSites(sandbox);
+
+ info("TopSites.insert should pin site in first slot of empty pinned list");
+ Screenshots.getScreenshotForURL.resolves(Promise.resolve(null));
+ await TopSites.getScreenshotPreview("custom", 1234);
+
+ Assert.ok(TopSites.store.dispatch.calledOnce);
+ Assert.ok(
+ TopSites.store.dispatch.calledWithExactly(
+ ac.OnlyToOneContent(
+ {
+ data: { preview: "", url: "custom" },
+ type: at.PREVIEW_RESPONSE,
+ },
+ 1234
+ )
+ )
+ );
+
+ Screenshots.getScreenshotForURL.resolves(FAKE_SCREENSHOT);
+
+ {
+ info(
+ "TopSites.insert should pin site in first slot of pinned list with " +
+ "empty first slot"
+ );
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, { url: "example.com" }]);
+ let site = { url: "foo.bar", label: "foo" };
+ await TopSites.insert({ data: { site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.insert should move a pinned site in first slot to the " +
+ "next slot: part 1"
+ );
+ let site1 = { url: "example.com" };
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [site1]);
+ let site = { url: "foo.bar", label: "foo" };
+
+ await TopSites.insert({ data: { site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.insert should move a pinned site in first slot to the " +
+ "next slot: part 2"
+ );
+ let site1 = { url: "example.com" };
+ let site2 = { url: "example.org" };
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [site1, null, site2]);
+ let site = { url: "foo.bar", label: "foo" };
+ await TopSites.insert({ data: { site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_insert_part_2() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ let cleanup = stubTopSites(sandbox);
+
+ {
+ info(
+ "TopSites.insert should unpin the last site if all slots are " +
+ "already pinned"
+ );
+ let site1 = { url: "example.com" };
+ let site2 = { url: "example.org" };
+ let site3 = { url: "example.net" };
+ let site4 = { url: "example.biz" };
+ let site5 = { url: "example.info" };
+ let site6 = { url: "example.news" };
+ let site7 = { url: "example.lol" };
+ let site8 = { url: "example.golf" };
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [site1, site2, site3, site4, site5, site6, site7, site8]);
+ TopSites.store.state.Prefs.values.topSitesRows = 1;
+ let site = { url: "foo.bar", label: "foo" };
+ await TopSites.insert({ data: { site } });
+ Assert.equal(
+ NewTabUtils.pinnedLinks.pin.callCount,
+ 8,
+ "NewTabUtils.pinnedLinks.pin called 8 times"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 1));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 2));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site3, 3));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site4, 4));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site5, 5));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site6, 6));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site7, 7));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info("TopSites.insert should trigger refresh on TOP_SITES_INSERT");
+ sandbox.stub(TopSites, "refresh");
+ let addAction = {
+ type: at.TOP_SITES_INSERT,
+ data: { site: { url: "foo.com" } },
+ };
+
+ await TopSites.insert(addAction);
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ }
+
+ {
+ info("TopSites.insert should correctly handle different index values");
+ let index = -1;
+ let site = { url: "foo.bar", label: "foo" };
+ let action = { data: { index, site } };
+
+ await TopSites.insert(action);
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+
+ index = undefined;
+ await TopSites.insert(action);
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 0));
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_insert_part_3() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ let cleanup = stubTopSites(sandbox);
+
+ {
+ info("TopSites.insert should pin site in specified slot that is free");
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo" };
+
+ await TopSites.insert({ data: { index: 2, site, draggedFromIndex: 0 } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.insert should move a pinned site in specified slot " +
+ "to the next slot"
+ );
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo" };
+
+ await TopSites.insert({ data: { index: 2, site, draggedFromIndex: 3 } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledWith({ url: "example.com" }, 3)
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.insert should move pinned sites in the direction " +
+ "of the dragged site"
+ );
+
+ let site1 = { url: "foo.bar", label: "foo" };
+ let site2 = { url: "example.com", label: "example" };
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, null, site2]);
+
+ await TopSites.insert({
+ data: { index: 2, site: site1, draggedFromIndex: 0 },
+ });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 2));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 1));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+
+ await TopSites.insert({
+ data: { index: 2, site: site1, draggedFromIndex: 5 },
+ });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site1, 2));
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site2, 3));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ {
+ info("TopSites.insert should not insert past the visible top sites");
+ let site1 = { url: "foo.bar", label: "foo" };
+ await TopSites.insert({
+ data: { index: 42, site: site1, draggedFromIndex: 0 },
+ });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.notCalled,
+ "NewTabUtils.pinnedLinks.pin wasn't called"
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_pin_part_1() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.spy(TopSites.pinnedCache, "request");
+ let cleanup = stubTopSites(sandbox);
+
+ {
+ info("TopSites.pin should pin site in specified slot empty pinned list");
+ let site = {
+ url: "foo.bar",
+ label: "foo",
+ customScreenshotURL: "screenshot",
+ };
+ Assert.ok(
+ TopSites.pinnedCache.request.notCalled,
+ "TopSites.pinnedCache.request not called"
+ );
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.called,
+ "NewTabUtils.pinnedLinks.pin called"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.pinnedCache.request.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.pin should lookup the link object to update the custom " +
+ "screenshot"
+ );
+ let site = {
+ url: "foo.bar",
+ label: "foo",
+ customScreenshotURL: "screenshot",
+ };
+ Assert.ok(
+ TopSites.pinnedCache.request.notCalled,
+ "TopSites.pinnedCache.request not called"
+ );
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ TopSites.pinnedCache.request.called,
+ "TopSites.pinnedCache.request called"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.pinnedCache.request.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.pin should lookup the link object to update the custom " +
+ "screenshot when the custom screenshot is initially null"
+ );
+ let site = {
+ url: "foo.bar",
+ label: "foo",
+ customScreenshotURL: null,
+ };
+ Assert.ok(
+ TopSites.pinnedCache.request.notCalled,
+ "TopSites.pinnedCache.request not called"
+ );
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ TopSites.pinnedCache.request.called,
+ "TopSites.pinnedCache.request called"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.pinnedCache.request.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.pin should not do a link object lookup if custom " +
+ "screenshot field is not set"
+ );
+ let site = { url: "foo.bar", label: "foo" };
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ !TopSites.pinnedCache.request.called,
+ "TopSites.pinnedCache.request never called"
+ );
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.pinnedCache.request.resetHistory();
+ }
+
+ {
+ info(
+ "TopSites.pin should pin site in specified slot of pinned " +
+ "list that is free"
+ );
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo" };
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ }
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_pin_part_2() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+
+ {
+ info("TopSites.pin should save the searchTopSite attribute if set");
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo", searchTopSite: true };
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.firstCall.args[0].searchTopSite);
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.pin should NOT move a pinned site in specified " +
+ "slot to the next slot"
+ );
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [null, null, { url: "example.com" }]);
+
+ let site = { url: "foo.bar", label: "foo" };
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.pin({ data: { index: 2, site } });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(NewTabUtils.pinnedLinks.pin.calledWith(site, 2));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.pin should properly update LinksCache object " +
+ "properties between migrations"
+ );
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "https://foo.com/" }]);
+
+ let cleanup = stubTopSites(sandbox);
+ let pinnedLinks = await TopSites.pinnedCache.request();
+ Assert.equal(pinnedLinks.length, 1);
+ TopSites.pinnedCache.expire();
+
+ pinnedLinks[0].__sharedCache.updateLink("screenshot", "foo");
+
+ pinnedLinks = await TopSites.pinnedCache.request();
+ Assert.equal(pinnedLinks[0].screenshot, "foo");
+
+ // Force cache expiration in order to trigger a migration of objects
+ TopSites.pinnedCache.expire();
+ pinnedLinks[0].__sharedCache.updateLink("screenshot", "bar");
+
+ pinnedLinks = await TopSites.pinnedCache.request();
+ Assert.equal(pinnedLinks[0].screenshot, "bar");
+ await cleanup();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_pin_part_3() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.spy(TopSites, "insert");
+
+ {
+ info("TopSites.pin should call insert if index < 0");
+ let site = { url: "foo.bar", label: "foo" };
+ let action = { data: { index: -1, site } };
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.pin(action);
+
+ Assert.ok(TopSites.insert.calledOnce, "TopSites.insert called once");
+ Assert.ok(TopSites.insert.calledWithExactly(action));
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ TopSites.insert.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info("TopSites.pin should not call insert if index == 0");
+ let site = { url: "foo.bar", label: "foo" };
+ let action = { data: { index: 0, site } };
+ let cleanup = stubTopSites(sandbox);
+ await TopSites.pin(action);
+
+ Assert.ok(!TopSites.insert.called, "TopSites.insert not called");
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info("TopSites.pin should trigger refresh on TOP_SITES_PIN");
+ let cleanup = stubTopSites(sandbox);
+ sandbox.stub(TopSites, "refresh");
+ let pinExistingAction = {
+ type: at.TOP_SITES_PIN,
+ data: { site: FAKE_LINKS[4], index: 4 },
+ };
+
+ await TopSites.pin(pinExistingAction);
+
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ await cleanup();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_integration() {
+ let sandbox = sinon.createSandbox();
+
+ info("Test adding a pinned site and removing it with actions");
+ let cleanup = stubTopSites(sandbox);
+
+ let resolvers = [];
+ TopSites.store.dispatch = sandbox.stub().callsFake(() => {
+ resolvers.shift()();
+ });
+ TopSites._startedUp = true;
+ sandbox.stub(TopSites, "_fetchScreenshot");
+
+ let forDispatch = action =>
+ new Promise(resolve => {
+ resolvers.push(resolve);
+ TopSites.onAction(action);
+ });
+
+ TopSites._requestRichIcon = sandbox.stub();
+ let url = "https://pin.me";
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin").callsFake(link => {
+ NewTabUtils.pinnedLinks.links.push(link);
+ });
+
+ await forDispatch({ type: at.TOP_SITES_INSERT, data: { site: { url } } });
+ NewTabUtils.pinnedLinks.links.pop();
+ await forDispatch({ type: at.PLACES_LINK_BLOCKED });
+
+ Assert.ok(
+ TopSites.store.dispatch.calledTwice,
+ "TopSites.store.dispatch called twice"
+ );
+ Assert.equal(
+ TopSites.store.dispatch.firstCall.args[0].data.links[0].url,
+ url
+ );
+ Assert.equal(
+ TopSites.store.dispatch.secondCall.args[0].data.links[0].url,
+ FAKE_LINKS[0].url
+ );
+
+ sandbox.restore();
+ await cleanup();
+});
+
+add_task(async function test_improvesearch_noDefaultSearchTile_experiment() {
+ let sandbox = sinon.createSandbox();
+ const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
+
+ sandbox.stub(SearchService.prototype, "getDefault").resolves({
+ identifier: "google",
+ searchForm: "google.com",
+ });
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should filter out alexa top 5 " +
+ "search from the default sites"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+ let top5Test = [
+ "https://google.com",
+ "https://search.yahoo.com",
+ "https://yahoo.com",
+ "https://bing.com",
+ "https://ask.com",
+ "https://duckduckgo.com",
+ ];
+
+ gGetTopSitesStub.resolves([
+ { url: "https://amazon.com" },
+ ...top5Test.map(url => ({ url })),
+ ]);
+
+ const urlsReturned = (await TopSites.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(
+ urlsReturned.includes("https://amazon.com"),
+ "amazon included in default links"
+ );
+ top5Test.forEach(url =>
+ Assert.ok(!urlsReturned.includes(url), `Should not include ${url}`)
+ );
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should not filter out alexa, default " +
+ "search from the query results if the experiment pref is off"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = false;
+
+ gGetTopSitesStub.resolves([
+ { url: "https://google.com" },
+ { url: "https://foo.com" },
+ { url: "https://duckduckgo" },
+ ]);
+ let urlsReturned = (await TopSites.getLinksWithDefaults()).map(
+ link => link.url
+ );
+
+ Assert.ok(urlsReturned.includes("https://google.com"));
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should filter out the current " +
+ "default search from the default sites"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+
+ sandbox.stub(TopSites, "_currentSearchHostname").get(() => "amazon");
+ TopSites.onAction({
+ type: at.PREFS_INITIAL_VALUES,
+ data: { "default.sites": "google.com,amazon.com" },
+ });
+ gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
+
+ let urlsReturned = (await TopSites.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(!urlsReturned.includes("https://amazon.com"));
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should not filter out current " +
+ "default search from pinned sites even if it matches the current " +
+ "default search"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+
+ sandbox
+ .stub(NewTabUtils.pinnedLinks, "links")
+ .get(() => [{ url: "google.com" }]);
+ gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
+
+ let urlsReturned = (await TopSites.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(urlsReturned.includes("google.com"));
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ await cleanup();
+ }
+
+ sandbox.restore();
+});
+
+add_task(
+ async function test_improvesearch_noDefaultSearchTile_experiment_part_2() {
+ let sandbox = sinon.createSandbox();
+ const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
+
+ sandbox.stub(SearchService.prototype, "getDefault").resolves({
+ identifier: "google",
+ searchForm: "google.com",
+ });
+
+ sandbox.stub(TopSites, "refresh");
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should call refresh and set " +
+ "._currentSearchHostname to the new engine hostname when the " +
+ "default search engine has been set"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+
+ TopSites.observe(
+ null,
+ "browser-search-engine-modified",
+ "engine-default"
+ );
+ Assert.equal(TopSites._currentSearchHostname, "duckduckgo");
+ Assert.ok(TopSites.refresh.calledOnce, "TopSites.refresh called once");
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ TopSites.refresh.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.getLinksWithDefaults should call refresh when the " +
+ "experiment pref has changed"
+ );
+ let cleanup = stubTopSites(sandbox);
+ TopSites.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
+
+ TopSites.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: true },
+ });
+ Assert.ok(
+ TopSites.refresh.calledOnce,
+ "TopSites.refresh was called once"
+ );
+
+ TopSites.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: NO_DEFAULT_SEARCH_TILE_PREF, value: false },
+ });
+ Assert.ok(
+ TopSites.refresh.calledTwice,
+ "TopSites.refresh was called twice"
+ );
+
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ TopSites.refresh.resetHistory();
+ await cleanup();
+ }
+
+ sandbox.restore();
+ }
+);
+
+// eslint-disable-next-line max-statements
+add_task(async function test_improvesearch_topSitesSearchShortcuts() {
+ let sandbox = sinon.createSandbox();
+ let searchEngines = [{ aliases: ["@google"] }, { aliases: ["@amazon"] }];
+ sandbox
+ .stub(SearchService.prototype, "getAppProvidedEngines")
+ .resolves(searchEngines);
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin").callsFake((site, index) => {
+ NewTabUtils.pinnedLinks.links[index] = site;
+ });
+
+ let prepTopSites = () => {
+ TopSites.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = true;
+ TopSites.store.state.Prefs.values[SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF] =
+ "google,amazon";
+ TopSites.store.state.Prefs.values[SEARCH_SHORTCUTS_HAVE_PINNED_PREF] = "";
+ };
+
+ {
+ info(
+ "TopSites should updateCustomSearchShortcuts when experiment " +
+ "pref is turned on"
+ );
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ TopSites.store.state.Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT_PREF] = false;
+ sandbox.spy(TopSites, "updateCustomSearchShortcuts");
+
+ // turn the experiment on
+ TopSites.onAction({
+ type: at.PREF_CHANGED,
+ data: { name: SEARCH_SHORTCUTS_EXPERIMENT_PREF, value: true },
+ });
+
+ Assert.ok(
+ TopSites.updateCustomSearchShortcuts.calledOnce,
+ "TopSites.updateCustomSearchShortcuts called once"
+ );
+ TopSites.updateCustomSearchShortcuts.restore();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites should filter out default top sites that match a " +
+ "hostname of a search shortcut if previously blocked"
+ );
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ TopSites.refreshDefaults("https://amazon.ca");
+ sandbox
+ .stub(NewTabUtils.blockedLinks, "links")
+ .value([{ url: "https://amazon.com" }]);
+ sandbox.stub(NewTabUtils.blockedLinks, "isBlocked").callsFake(site => {
+ return NewTabUtils.blockedLinks.links[0].url === site.url;
+ });
+
+ let urlsReturned = (await TopSites.getLinksWithDefaults()).map(
+ link => link.url
+ );
+ Assert.ok(!urlsReturned.includes("https://amazon.ca"));
+ await cleanup();
+ }
+
+ {
+ info("TopSites should update frecent search topsite icon");
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ sandbox.stub(TopSites._tippyTopProvider, "processSite").callsFake(site => {
+ site.tippyTopIcon = "icon.png";
+ site.backgroundColor = "#fff";
+ return site;
+ });
+ gGetTopSitesStub.resolves([{ url: "https://google.com" }]);
+
+ let urlsReturned = await TopSites.getLinksWithDefaults();
+
+ let defaultSearchTopsite = urlsReturned.find(
+ s => s.url === "https://google.com"
+ );
+ Assert.ok(defaultSearchTopsite.searchTopSite);
+ Assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
+ Assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ TopSites._tippyTopProvider.processSite.restore();
+ await cleanup();
+ }
+
+ {
+ info("TopSites should update default search topsite icon");
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ sandbox.stub(TopSites._tippyTopProvider, "processSite").callsFake(site => {
+ site.tippyTopIcon = "icon.png";
+ site.backgroundColor = "#fff";
+ return site;
+ });
+ gGetTopSitesStub.resolves([{ url: "https://foo.com" }]);
+ TopSites.onAction({
+ type: at.PREFS_INITIAL_VALUES,
+ data: { "default.sites": "google.com,amazon.com" },
+ });
+
+ let urlsReturned = await TopSites.getLinksWithDefaults();
+
+ let defaultSearchTopsite = urlsReturned.find(
+ s => s.url === "https://amazon.com"
+ );
+ Assert.ok(defaultSearchTopsite.searchTopSite);
+ Assert.equal(defaultSearchTopsite.tippyTopIcon, "icon.png");
+ Assert.equal(defaultSearchTopsite.backgroundColor, "#fff");
+ gGetTopSitesStub.resolves(FAKE_LINKS);
+ TopSites._tippyTopProvider.processSite.restore();
+ TopSites.store.dispatch.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites should dispatch UPDATE_SEARCH_SHORTCUTS on " +
+ "updateCustomSearchShortcuts"
+ );
+ let cleanup = stubTopSites(sandbox);
+ prepTopSites();
+ TopSites.store.state.Prefs.values[
+ "improvesearch.noDefaultSearchTile"
+ ] = true;
+ await TopSites.updateCustomSearchShortcuts();
+ Assert.ok(
+ TopSites.store.dispatch.calledOnce,
+ "TopSites.store.dispatch called once"
+ );
+ Assert.ok(
+ TopSites.store.dispatch.calledWith({
+ data: {
+ searchShortcuts: [
+ {
+ keyword: "@google",
+ shortURL: "google",
+ url: "https://google.com",
+ backgroundColor: undefined,
+ smallFavicon:
+ "chrome://activity-stream/content/data/content/tippytop/favicons/google-com.ico",
+ tippyTopIcon:
+ "chrome://activity-stream/content/data/content/tippytop/images/google-com@2x.png",
+ },
+ {
+ keyword: "@amazon",
+ shortURL: "amazon",
+ url: "https://amazon.com",
+ backgroundColor: undefined,
+ smallFavicon:
+ "chrome://activity-stream/content/data/content/tippytop/favicons/amazon.ico",
+ tippyTopIcon:
+ "chrome://activity-stream/content/data/content/tippytop/images/amazon@2x.png",
+ },
+ ],
+ },
+ meta: {
+ from: "ActivityStream:Main",
+ to: "ActivityStream:Content",
+ isStartup: false,
+ },
+ type: "UPDATE_SEARCH_SHORTCUTS",
+ })
+ );
+ await cleanup();
+ }
+
+ sandbox.restore();
+});
+
+add_task(async function test_updatePinnedSearchShortcuts() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(NewTabUtils.pinnedLinks, "pin");
+ sandbox.stub(NewTabUtils.pinnedLinks, "unpin");
+
+ {
+ info(
+ "TopSites.updatePinnedSearchShortcuts should unpin a " +
+ "shortcut in deletedShortcuts"
+ );
+ let cleanup = stubTopSites(sandbox);
+
+ let deletedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ let addedShortcuts = [];
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
+ null,
+ null,
+ {
+ url: "https://amazon.com",
+ searchVendor: "amazon",
+ label: "amazon",
+ searchTopSite: true,
+ },
+ ]);
+ TopSites.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.notCalled,
+ "NewTabUtils.pinnedLinks.pin not called"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.calledOnce,
+ "NewTabUtils.pinnedLinks.unpin called once"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.calledWith({
+ url: "https://google.com",
+ })
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ NewTabUtils.pinnedLinks.unpin.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.updatePinnedSearchShortcuts should pin a shortcut " +
+ "in addedShortcuts"
+ );
+ let cleanup = stubTopSites(sandbox);
+
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ let deletedShortcuts = [];
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
+ null,
+ null,
+ {
+ url: "https://amazon.com",
+ searchVendor: "amazon",
+ label: "amazon",
+ searchTopSite: true,
+ },
+ ]);
+ TopSites.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.notCalled,
+ "NewTabUtils.pinnedLinks.unpin not called"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledOnce,
+ "NewTabUtils.pinnedLinks.pin called once"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledWith(
+ {
+ label: "google",
+ searchTopSite: true,
+ searchVendor: "google",
+ url: "https://google.com",
+ },
+ 0
+ )
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ NewTabUtils.pinnedLinks.unpin.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.updatePinnedSearchShortcuts should pin and unpin " +
+ "in the same action"
+ );
+ let cleanup = stubTopSites(sandbox);
+
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ {
+ url: "https://ebay.com",
+ searchVendor: "ebay",
+ label: "ebay",
+ searchTopSite: true,
+ },
+ ];
+ let deletedShortcuts = [
+ {
+ url: "https://amazon.com",
+ searchVendor: "amazon",
+ label: "amazon",
+ searchTopSite: true,
+ },
+ ];
+
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => [
+ { url: "https://foo.com" },
+ {
+ url: "https://amazon.com",
+ searchVendor: "amazon",
+ label: "amazon",
+ searchTopSite: true,
+ },
+ ]);
+ TopSites.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
+
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.calledOnce,
+ "NewTabUtils.pinnedLinks.unpin called once"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledTwice,
+ "NewTabUtils.pinnedLinks.pin called twice"
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ NewTabUtils.pinnedLinks.unpin.resetHistory();
+ await cleanup();
+ }
+
+ {
+ info(
+ "TopSites.updatePinnedSearchShortcuts should pin a shortcut in " +
+ "addedShortcuts even if pinnedLinks is full"
+ );
+ let cleanup = stubTopSites(sandbox);
+
+ let addedShortcuts = [
+ {
+ url: "https://google.com",
+ searchVendor: "google",
+ label: "google",
+ searchTopSite: true,
+ },
+ ];
+ let deletedShortcuts = [];
+ sandbox.stub(NewTabUtils.pinnedLinks, "links").get(() => FAKE_LINKS);
+ TopSites.updatePinnedSearchShortcuts({ addedShortcuts, deletedShortcuts });
+
+ Assert.ok(
+ NewTabUtils.pinnedLinks.unpin.notCalled,
+ "NewTabUtils.pinnedLinks.unpin not called"
+ );
+ Assert.ok(
+ NewTabUtils.pinnedLinks.pin.calledWith(
+ { label: "google", searchTopSite: true, url: "https://google.com" },
+ 0
+ ),
+ "NewTabUtils.pinnedLinks.unpin not called"
+ );
+
+ NewTabUtils.pinnedLinks.pin.resetHistory();
+ NewTabUtils.pinnedLinks.unpin.resetHistory();
+ await cleanup();
+ }
+
+ sandbox.restore();
+});
+
+// eslint-disable-next-line max-statements
+add_task(async function test_ContileIntegration() {
+ let sandbox = sinon.createSandbox();
+ Services.prefs.setStringPref(
+ TOP_SITES_BLOCKED_SPONSORS_PREF,
+ `["foo","bar"]`
+ );
+ sandbox.stub(NimbusFeatures.newtab, "getVariable").returns(true);
+
+ let fetchStub;
+
+ let prepTopSites = () => {
+ TopSites.store.state.Prefs.values[SHOW_SPONSORED_PREF] = true;
+ fetchStub = sandbox.stub(TopSites, "fetch");
+ function cleanupPrep() {
+ TopSites._contile._sites = [];
+ fetchStub.restore();
+ }
+ return cleanupPrep;
+ };
+
+ {
+ info("TopSites._fetchSites should fetch sites from Contile");
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://www.test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 2);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info("TopSites._fetchSites should call allocatePositions");
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ sandbox.stub(TopSites, "allocatePositions").resolves();
+ await TopSites._contile.refresh();
+
+ Assert.ok(
+ TopSites.allocatePositions.calledOnce,
+ "TopSites.allocatePositions called once"
+ );
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should fetch SOV (Share-of-Voice) " +
+ "settings from Contile"
+ );
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ let sov = {
+ name: "SOV-20230518215316",
+ allocations: [
+ {
+ position: 1,
+ allocation: [
+ {
+ partner: "foo",
+ percentage: 100,
+ },
+ {
+ partner: "bar",
+ percentage: 0,
+ },
+ ],
+ },
+ {
+ position: 2,
+ allocation: [
+ {
+ partner: "foo",
+ percentage: 80,
+ },
+ {
+ partner: "bar",
+ percentage: 20,
+ },
+ ],
+ },
+ ],
+ };
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ sov: btoa(JSON.stringify(sov)),
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://www.test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.deepEqual(TopSites._contile.sov, sov);
+ Assert.equal(TopSites._contile.sites.length, 2);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should not fetch from Contile if " +
+ "it's not enabled"
+ );
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ NimbusFeatures.newtab.getVariable.reset();
+ NimbusFeatures.newtab.getVariable.returns(false);
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(fetchStub.notCalled, "TopSites.fetch was not called");
+ Assert.ok(!fetched);
+ Assert.equal(TopSites._contile.sites.length, 0);
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should still return two tiles when Contile " +
+ "provides more than 2 tiles and filtering results in more than 2 tiles"
+ );
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ NimbusFeatures.newtab.getVariable.reset();
+ NimbusFeatures.newtab.getVariable.onCall(0).returns(true);
+ NimbusFeatures.newtab.getVariable.onCall(1).returns(true);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://foo.com",
+ image_url: "images/foo-com.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://bar.com",
+ image_url: "images/bar-com.png",
+ click_url: "https://www.bar-click.com",
+ impression_url: "https://www.bar-impression.com",
+ name: "bar",
+ },
+ {
+ url: "https://test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ {
+ url: "https://test2.com",
+ image_url: "images/test2-com.png",
+ click_url: "https://www.test2-click.com",
+ impression_url: "https://www.test2-impression.com",
+ name: "test2",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ // Both "foo" and "bar" should be filtered
+ Assert.equal(TopSites._contile.sites.length, 2);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test.com");
+ Assert.equal(TopSites._contile.sites[1].url, "https://test1.com");
+
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should still return two tiles with " +
+ "replacement if the Nimbus variable was unset"
+ );
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ NimbusFeatures.newtab.getVariable.reset();
+ NimbusFeatures.newtab.getVariable.onCall(0).returns(true);
+ NimbusFeatures.newtab.getVariable.onCall(1).returns(undefined);
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://foo.com",
+ image_url: "images/foo-com.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 2);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test.com");
+ Assert.equal(TopSites._contile.sites[1].url, "https://test1.com");
+
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info("TopSites._fetchSites should filter the blocked sponsors");
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://foo.com",
+ image_url: "images/foo-com.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://bar.com",
+ image_url: "images/bar-com.png",
+ click_url: "https://www.bar-click.com",
+ impression_url: "https://www.bar-impression.com",
+ name: "bar",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ // Both "foo" and "bar" should be filtered
+ Assert.equal(TopSites._contile.sites.length, 1);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test.com");
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should return false when Contile returns " +
+ "with error status and no values are stored in cache prefs"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, "[]");
+ Services.prefs.setIntPref(CONTILE_CACHE_LAST_FETCH_PREF, 0);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ fetchStub.resolves({
+ ok: false,
+ status: 500,
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should return false when Contile " +
+ "returns with error status and cached tiles are expried"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, "[]");
+ const THIRTY_MINUTES_AGO_IN_SECONDS =
+ Math.round(Date.now() / 1000) - 60 * 30;
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ THIRTY_MINUTES_AGO_IN_SECONDS
+ );
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 60 * 15);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ fetchStub.resolves({
+ ok: false,
+ status: 500,
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should handle invalid payload " +
+ "properly from Contile"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ json: () =>
+ Promise.resolve({
+ unknown: [],
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should handle empty payload properly " +
+ "from Contile"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [],
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info("TopSites._fetchSites should handle no content properly from Contile");
+ NimbusFeatures.newtab.getVariable.returns(true);
+
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ fetchStub.resolves({ ok: true, status: 204 });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(!fetched);
+ Assert.ok(!TopSites._contile.sites.length);
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should set Caching Prefs after " +
+ "a successful request"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ let tiles = [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://www.test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ];
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles,
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(
+ Services.prefs.getStringPref(CONTILE_CACHE_PREF),
+ JSON.stringify(tiles)
+ );
+ Assert.equal(
+ Services.prefs.getIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF),
+ 11322
+ );
+ await cleanup();
+ cleanupPrep();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should return cached valid tiles " +
+ "when Contile returns error status"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ let tiles = [
+ {
+ url: "https://www.test-cached.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://www.test1-cached.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ];
+
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, JSON.stringify(tiles));
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 60 * 15);
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ Math.round(Date.now() / 1000)
+ );
+
+ fetchStub.resolves({
+ status: 304,
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 2);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test-cached.com");
+ Assert.equal(
+ TopSites._contile.sites[1].url,
+ "https://www.test1-cached.com"
+ );
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should not be successful when contile " +
+ "returns an error and no valid tiles are cached"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, "[]");
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 0);
+ Services.prefs.setIntPref(CONTILE_CACHE_LAST_FETCH_PREF, 0);
+
+ fetchStub.resolves({
+ status: 500,
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+ Assert.ok(!fetched);
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should return cached valid tiles " +
+ "filtering blocked tiles when Contile returns error status"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ let tiles = [
+ {
+ url: "https://foo.com",
+ image_url: "images/foo-com.png",
+ click_url: "https://www.foo-click.com",
+ impression_url: "https://www.foo-impression.com",
+ name: "foo",
+ },
+ {
+ url: "https://www.test1-cached.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ ];
+ Services.prefs.setStringPref(CONTILE_CACHE_PREF, JSON.stringify(tiles));
+ Services.prefs.setIntPref(CONTILE_CACHE_VALID_FOR_SECONDS_PREF, 60 * 15);
+ Services.prefs.setIntPref(
+ CONTILE_CACHE_LAST_FETCH_PREF,
+ Math.round(Date.now() / 1000)
+ );
+
+ fetchStub.resolves({
+ status: 304,
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 1);
+ Assert.equal(
+ TopSites._contile.sites[0].url,
+ "https://www.test1-cached.com"
+ );
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ {
+ info(
+ "TopSites._fetchSites should still return 3 tiles when nimbus " +
+ "variable overrides max num of sponsored contile tiles"
+ );
+ NimbusFeatures.newtab.getVariable.returns(true);
+ let cleanup = stubTopSites(sandbox);
+ let cleanupPrep = prepTopSites();
+
+ sandbox.stub(NimbusFeatures.pocketNewtab, "getVariable").returns(3);
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ headers: new Map([
+ ["cache-control", "private, max-age=859, stale-if-error=10463"],
+ ]),
+ json: () =>
+ Promise.resolve({
+ tiles: [
+ {
+ url: "https://www.test.com",
+ image_url: "images/test-com.png",
+ click_url: "https://www.test-click.com",
+ impression_url: "https://www.test-impression.com",
+ name: "test",
+ },
+ {
+ url: "https://test1.com",
+ image_url: "images/test1-com.png",
+ click_url: "https://www.test1-click.com",
+ impression_url: "https://www.test1-impression.com",
+ name: "test1",
+ },
+ {
+ url: "https://test2.com",
+ image_url: "images/test2-com.png",
+ click_url: "https://www.test2-click.com",
+ impression_url: "https://www.test2-impression.com",
+ name: "test2",
+ },
+ ],
+ }),
+ });
+
+ let fetched = await TopSites._contile._fetchSites();
+
+ Assert.ok(fetched);
+ Assert.equal(TopSites._contile.sites.length, 3);
+ Assert.equal(TopSites._contile.sites[0].url, "https://www.test.com");
+ Assert.equal(TopSites._contile.sites[1].url, "https://test1.com");
+ Assert.equal(TopSites._contile.sites[2].url, "https://test2.com");
+ await cleanup();
+ cleanupPrep();
+ fetchStub.restore();
+ }
+
+ Services.prefs.clearUserPref(TOP_SITES_BLOCKED_SPONSORS_PREF);
+ sandbox.restore();
+});
diff --git a/browser/components/topsites/test/unit/xpcshell.toml b/browser/components/topsites/test/unit/xpcshell.toml
new file mode 100644
index 0000000000..98b0fc5360
--- /dev/null
+++ b/browser/components/topsites/test/unit/xpcshell.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+firefox-appdir = "browser"
+
+["test_top_sites.js"]
diff --git a/browser/components/touchbar/MacTouchBar.sys.mjs b/browser/components/touchbar/MacTouchBar.sys.mjs
index 5b598efc00..1969317e12 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: {
@@ -134,7 +134,7 @@ var gBuiltInInputs = {
type: kInputTypes.BUTTON,
callback: () => {
let win = lazy.BrowserWindowTracker.getTopWindow();
- win.SidebarUI.toggle();
+ win.SidebarController.toggle();
},
},
AddBookmark: {
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.inc.xhtml b/browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml
index bc0c5b319f..6b3e19538d 100644
--- a/browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml
+++ b/browser/components/translations/content/fullPageTranslationsPanel.inc.xhtml
@@ -24,7 +24,7 @@
<html:h1 class="translations-panel-header-wrapper">
<html:span id="full-page-translations-panel-header"></html:span>
</html:h1>
- <hbox class="translations-panel-beta">
+ <hbox class="translations-panel-beta" role="image" aria-label="Beta">
<image class="translations-panel-beta-icon"></image>
</hbox>
<toolbarbutton id="translations-panel-settings"
@@ -49,7 +49,7 @@
flex="1"
value="detect"
size="large"
- aria-labelledby="translations-panel-from-label"
+ aria-labelledby="full-page-translations-panel-from-label"
oncommand="FullPageTranslationsPanel.onChangeFromLanguage(event)">
<menupopup id="full-page-translations-panel-from-menupopup"
class="translations-panel-language-menupopup-from">
@@ -63,7 +63,7 @@
flex="1"
value="detect"
size="large"
- aria-labelledby="translations-panel-to-label"
+ aria-labelledby="full-page-translations-panel-to-label"
oncommand="FullPageTranslationsPanel.onChangeToLanguage(event)">
<menupopup id="full-page-translations-panel-to-menupopup"
class="translations-panel-language-menupopup-to">
@@ -85,7 +85,7 @@
</vbox>
</vbox>
- <html:moz-button-group class="panel-footer translations-panel-footer">
+ <html:moz-button-group class="panel-footer translations-panel-footer translations-panel-button-group">
<button id="full-page-translations-panel-restore-button"
class="footer-button"
oncommand="FullPageTranslationsPanel.onRestore(event);"
diff --git a/browser/components/translations/content/fullPageTranslationsPanel.js b/browser/components/translations/content/fullPageTranslationsPanel.js
index 2e35440160..2875333d61 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 */
);
@@ -1047,8 +1041,6 @@ var FullPageTranslationsPanel = new (class {
isFirstUserInteraction = null,
}
) {
- await window.ensureCustomElements("moz-button-group");
-
const { panel, appMenuButton } = this.elements;
const openedFromAppMenu = target.id === appMenuButton.id;
const { docLangTag } = await this.#getCachedDetectedLanguages();
@@ -1113,14 +1105,12 @@ var FullPageTranslationsPanel = new (class {
return;
}
- const window =
- gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
- window.ensureCustomElements("moz-support-link");
-
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 +1122,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 +1137,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 +1155,9 @@ var FullPageTranslationsPanel = new (class {
*/
#isTranslationsActive() {
const { requestedTranslationPair } =
- this.#getTranslationsActor().languageState;
+ TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ ).languageState;
return requestedTranslationPair !== null;
}
@@ -1183,7 +1167,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 +1191,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 +1317,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 +1336,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..287bd65679 100644
--- a/browser/components/translations/content/selectTranslationsPanel.inc.xhtml
+++ b/browser/components/translations/content/selectTranslationsPanel.inc.xhtml
@@ -4,99 +4,162 @@
<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"
+ tabspecific="true"
+ locationspecific="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 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" role="image" aria-label="Beta">
+ <image id="select-translations-panel-beta-icon"
+ class="translations-panel-beta-icon">
+ </image>
+ </hbox>
+ <toolbarbutton id="select-translations-panel-settings-button"
+ class="panel-info-button translations-panel-settings-gear-icon"
+ data-l10n-id="translations-panel-settings-button"
+ tabindex="0"
+ closemenu="none"/>
+ </hbox>
+ <html:div id="select-translations-panel-init-failure-content"
+ class="translations-panel-content"
+ hidden="true">
+ <html:moz-message-bar id="select-translations-panel-init-failure-message-bar"
+ type="error"
+ class="select-translations-panel-message-bar"
+ data-l10n-id="select-translations-panel-init-failure-message"
+ data-l10n-attrs="message">
+ </html:moz-message-bar>
+ </html:div>
+ <html:div id="select-translations-panel-unsupported-language-content" hidden="true">
+ <vbox flex="1" class="select-translations-panel-content">
+ <html:moz-message-bar id="select-translations-panel-unsupported-language-message-bar"
+ type="info"
+ class="select-translations-panel-message-bar"
+ data-l10n-id="select-translations-panel-unsupported-language-message-unknown"
+ data-l10n-attrs="message">
+ </html:moz-message-bar>
+ <label id="select-translations-panel-try-another-language-label"
+ class="select-translations-panel-label"
+ data-l10n-id="select-translations-panel-try-another-language-label">
+ </label>
+ <menulist id="select-translations-panel-try-another-language"
+ flex="1"
+ value=""
+ size="large"
+ data-l10n-id="translations-panel-choose-language"
+ aria-labelledby="select-translations-panel-try-another-language-label"
+ noinitialselection="true">
+ <menupopup id="select-translations-panel-try-another-language-menupopup"
+ class="translations-panel-language-menupopup-from">
+ <!-- The list of <menuitem> will be dynamically inserted. -->
+ </menupopup>
+ </menulist>
+ </vbox>
+ </html:div>
+ <html:div id="select-translations-panel-main-content">
+ <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">
+ <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=""
+ size="large"
+ data-l10n-id="translations-panel-choose-language"
+ aria-labelledby="select-translations-panel-to-label"
+ noinitialselection="true">
+ <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>
- <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>
- </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>
-
- <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>
+ </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>
+ <html:moz-message-bar id="select-translations-panel-translation-failure-message-bar"
+ type="error"
+ hidden="true"
+ data-l10n-id="select-translations-panel-translation-failure-message"
+ data-l10n-attrs="message">
+ </html:moz-message-bar>
+ </vbox>
+ </html:div>
+ <html:div id="select-translations-panel-footer"
+ class="panel-footer translations-panel-footer">
+ <button id="select-translations-panel-copy-button"
+ class="footer-button select-translations-panel-copy-button"
+ data-l10n-id="select-translations-panel-copy-button">
+ </button>
+ <html:moz-button-group id="select-translations-panel-footer-button-group"
+ class="translations-panel-button-group">
+ <button id="select-translations-panel-cancel-button"
+ class="footer-button"
+ hidden="true"
+ data-l10n-id="select-translations-panel-cancel-button">
+ </button>
+ <button id="select-translations-panel-done-button-secondary"
+ hidden="true"
+ class="footer-button"
+ data-l10n-id="select-translations-panel-done-button">
+ </button>
+ <button id="select-translations-panel-translate-full-page-button"
+ class="footer-button"
+ data-l10n-id="select-translations-panel-translate-full-page-button">
+ </button>
+ <button id="select-translations-panel-done-button-primary"
+ class="footer-button"
+ data-l10n-id="select-translations-panel-done-button"
+ default="true">
+ </button>
+ <button id="select-translations-panel-translate-button"
+ class="footer-button"
+ data-l10n-id="select-translations-panel-translate-button"
+ hidden="true"
+ default="true"
+ disabled="true">
+ </button>
+ <button id="select-translations-panel-try-again-button"
+ class="footer-button"
+ data-l10n-id="select-translations-panel-try-again-button"
+ hidden="true"
+ default="true">
+ </button>
+ </html:moz-button-group>
+ </html:div>
</panel>
</html:template>
diff --git a/browser/components/translations/content/selectTranslationsPanel.js b/browser/components/translations/content/selectTranslationsPanel.js
index b4fe3e9735..36452d4cc0 100644
--- a/browser/components/translations/content/selectTranslationsPanel.js
+++ b/browser/components/translations/content/selectTranslationsPanel.js
@@ -4,15 +4,34 @@
/* 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",
});
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "ClipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
/**
- * 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 +59,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 +129,39 @@ var SelectTranslationsPanel = new (class {
#lazyElements;
/**
+ * Set to true the first time event listeners are initialized.
+ *
+ * @type {boolean}
+ */
+ #eventListenersInitialized = false;
+
+ /**
+ * This value is true if this page does not allow Full Page Translations,
+ * e.g. PDFs, reader mode, internal Firefox pages.
+ *
+ * Many of these are cases where the SelectTranslationsPanel is available
+ * even though the FullPageTranslationsPanel is not, so this helps inform
+ * whether the translate-full-page button should be allowed in this context.
+ */
+ #isFullPageTranslationsRestrictedForPage = true;
+
+ /**
+ * The internal state of the SelectTranslationsPanel.
+ *
+ * @type {SelectTranslationsPanelState}
+ */
+ #translationState = { phase: "closed" };
+
+ /**
+ * 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>}
@@ -73,17 +188,37 @@ var SelectTranslationsPanel = new (class {
TranslationsPanelShared.defineLazyElements(document, this.#lazyElements, {
betaIcon: "select-translations-panel-beta-icon",
+ cancelButton: "select-translations-panel-cancel-button",
copyButton: "select-translations-panel-copy-button",
- doneButton: "select-translations-panel-done-button",
+ doneButtonPrimary: "select-translations-panel-done-button-primary",
+ doneButtonSecondary: "select-translations-panel-done-button-secondary",
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",
+ initFailureContent: "select-translations-panel-init-failure-content",
+ initFailureMessageBar:
+ "select-translations-panel-init-failure-message-bar",
+ mainContent: "select-translations-panel-main-content",
+ settingsButton: "select-translations-panel-settings-button",
+ textArea: "select-translations-panel-text-area",
toLabel: "select-translations-panel-to-label",
toMenuList: "select-translations-panel-to",
+ toMenuPopup: "select-translations-panel-to-menupopup",
+ translateButton: "select-translations-panel-translate-button",
translateFullPageButton:
"select-translations-panel-translate-full-page-button",
+ translationFailureMessageBar:
+ "select-translations-panel-translation-failure-message-bar",
+ tryAgainButton: "select-translations-panel-try-again-button",
+ tryAnotherSourceMenuList:
+ "select-translations-panel-try-another-language",
+ tryAnotherSourceMenuPopup:
+ "select-translations-panel-try-another-language-menupopup",
+ unsupportedLanguageContent:
+ "select-translations-panel-unsupported-language-content",
+ unsupportedLanguageMessageBar:
+ "select-translations-panel-unsupported-language-message-bar",
});
}
@@ -91,27 +226,90 @@ 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.
+ try {
+ const actor = TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ );
+ const detectedLanguages = actor.languageState.detectedLanguages;
+ if (detectedLanguages?.isDocLangTagSupported) {
+ return detectedLanguages.docLangTag;
+ }
+ } catch (error) {
+ // Failed to retrieve the Translations actor to detect the document language.
+ // This is most likely due to attempting to retrieve the actor in a page that
+ // is restricted for Full Page Translations, such as a PDF or reader mode, but
+ // Select Translations is often still available, so we can safely continue to
+ // the final return fallback.
+ if (
+ !TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser)
+ ) {
+ // If we failed to retrieve the TranslationsParent actor on a non-restricted page,
+ // we should warn about this, because it is unexpected. The SelectTranslationsPanel
+ // itself will display an error state if this causes a failure, and this will help
+ // diagnose the issue if this scenario should ever occur.
+ this.console?.warn(
+ "Failed to retrieve the TranslationsParent actor on a page where Full Page Translations is not restricted."
+ );
+ this.console?.error(error);
+ }
+ }
+
+ // 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.
*
* @param {string} textToTranslate - The text for which the language detection and target language retrieval are performed.
* @returns {Promise<{fromLang?: string, toLang?: string}>} - An object containing the language pair for the translation.
* The `fromLang` property is omitted if it is a language that is not currently supported by Firefox Translations.
- * The `toLang` property is omitted if it is the same as `fromLang`.
*/
async getLangPairPromise(textToTranslate) {
+ if (
+ TranslationsParent.isInAutomation() &&
+ !TranslationsParent.isTranslationsEngineMocked()
+ ) {
+ // If we are in automation, and the Translations Engine is NOT mocked, then that means
+ // we are in a test case in which we are not explicitly testing Select Translations,
+ // and the code to get the supported languages below will not be available. However,
+ // we still need to ensure that the translate-selection menuitem in the context menu
+ // is compatible with all code in other tests, so we will return "en" for the purpose
+ // of being able to localize and display the context-menu item in other test cases.
+ return { toLang: "en" };
+ }
+
const [fromLang, toLang] = await Promise.all([
- LanguageDetector.detectLanguage(textToTranslate).then(
- ({ language }) => language
- ),
+ SelectTranslationsPanel.getTopSupportedDetectedLanguage(textToTranslate),
TranslationsParent.getTopPreferredSupportedToLang(),
]);
- return {
- fromLang,
- // If the fromLang and toLang are the same, discard the toLang.
- toLang: fromLang === toLang ? undefined : toLang,
- };
+ return { fromLang, toLang };
}
/**
@@ -122,99 +320,1402 @@ 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;
+ const {
+ fromMenuList,
+ fromMenuPopup,
+ toMenuList,
+ toMenuPopup,
+ tryAnotherSourceMenuList,
+ } = this.elements;
await Promise.all([
- this.#updateLanguageDropdown(fromLang, fromMenuList),
- this.#updateLanguageDropdown(toLang, toMenuList),
+ this.#initializeLanguageMenuList(fromLang, fromMenuList),
+ this.#initializeLanguageMenuList(toLang, toMenuList),
+ this.#initializeLanguageMenuList(null, tryAnotherSourceMenuList),
]);
+
+ this.#maybeTranslateOnEvents(["keypress"], fromMenuList);
+ this.#maybeTranslateOnEvents(["keypress"], toMenuList);
+
+ this.#maybeTranslateOnEvents(["popuphidden"], fromMenuPopup);
+ this.#maybeTranslateOnEvents(["popuphidden"], toMenuPopup);
}
/**
- * Opens the panel and populates the currently selected fromLang and toLang based
- * on the result of the langPairPromise.
+ * Initializes event listeners on the panel class the first time
+ * this function is called, and is a no-op on subsequent calls.
+ */
+ #initializeEventListeners() {
+ if (this.#eventListenersInitialized) {
+ // Event listeners have already been initialized, do nothing.
+ return;
+ }
+
+ const { panel, fromMenuList, toMenuList, tryAnotherSourceMenuList } =
+ this.elements;
+
+ panel.addEventListener("popupshown", this);
+ panel.addEventListener("popuphidden", this);
+
+ panel.addEventListener("command", this);
+ fromMenuList.addEventListener("command", this);
+ toMenuList.addEventListener("command", this);
+ tryAnotherSourceMenuList.addEventListener("command", this);
+
+ this.#eventListenersInitialized = true;
+ }
+
+ /**
+ * 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, screenX, screenY, sourceText, langPairPromise) {
+ if (this.#isOpen()) {
+ await this.#forceReopen(
+ event,
+ screenX,
+ screenY,
+ sourceText,
+ langPairPromise
+ );
+ return;
+ }
+
+ try {
+ this.#isFullPageTranslationsRestrictedForPage =
+ TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser);
+ this.#initializeEventListeners();
+ await this.#ensureLangListsBuilt();
+ await Promise.all([
+ this.#cachePlaceholderText(),
+ this.#initializeLanguageMenuLists(langPairPromise),
+ this.#registerSourceText(sourceText, langPairPromise),
+ ]);
+ this.#maybeRequestTranslation();
+ } catch (error) {
+ this.console?.error(error);
+ this.#changeStateToInitFailure(
+ event,
+ screenX,
+ screenY,
+ sourceText,
+ langPairPromise
+ );
+ }
+
+ this.#openPopup(event, screenX, screenY);
+ }
+
+ /**
+ * Forces the panel to close and reopen at the same location.
+ *
+ * This should never be called in the regular flow of events, but is good to have in case
+ * the panel somehow gets into an invalid state.
*
* @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 #forceReopen(event, screenX, screenY, sourceText, langPairPromise) {
+ this.console?.warn("The SelectTranslationsPanel was forced to reopen.");
+ this.close();
+ this.#changeStateToClosed();
+ await this.open(event, screenX, screenY, sourceText, langPairPromise);
+ }
+
+ /**
+ * 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.
+ */
+ #openPopup(event, screenX, screenY) {
+ 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.
+ * @param {Promise<{fromLang?: string, toLang?: string}>} langPairPromise
+ *
+ * @returns {Promise<void>}
+ */
+ async #registerSourceText(sourceText, langPairPromise) {
+ const { textArea } = this.elements;
+ const { fromLang, toLang } = await langPairPromise;
+ const isFromLangSupported = await TranslationsParent.isSupportedAsFromLang(
+ fromLang
+ );
+
+ if (isFromLangSupported) {
+ this.#changeStateTo("idle", /* retainEntries */ false, {
+ sourceText,
+ fromLanguage: fromLang,
+ toLanguage: toLang,
+ });
+ } else {
+ this.#changeStateTo("unsupported", /* retainEntries */ false, {
+ sourceText,
+ detectedLanguage: fromLang,
+ toLanguage: toLang,
+ });
+ }
+
+ if (sourceText.length < SelectTranslationsPanel.textLengthThreshold) {
+ textArea.style.height = SelectTranslationsPanel.shortTextHeight;
+ } else {
+ textArea.style.height = SelectTranslationsPanel.longTextHeight;
+ }
+
+ this.#maybeTranslateOnEvents(["focus"], textArea);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Opens the settings menu popup at the settings button gear-icon.
+ */
+ #openSettingsPopup() {
+ const { settingsButton } = this.elements;
+ const popup = settingsButton.ownerDocument.getElementById(
+ "select-translations-panel-settings-menupopup"
+ );
+ popup.openPopup(settingsButton, "after_start");
+ }
+
+ /**
+ * Opens the "About translation in Firefox" Mozilla support page in a new tab.
+ */
+ onAboutTranslations() {
+ this.close();
+ const window =
+ gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
+ window.openTrustedLinkIn(
+ "https://support.mozilla.org/kb/website-translation",
+ "tab",
+ {
+ forceForeground: true,
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ }
+ );
+ }
+
+ /**
+ * Opens the Translations section of about:preferences in a new tab.
+ */
+ openTranslationsSettingsPage() {
+ this.close();
+ const window =
+ gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
+ window.openTrustedLinkIn("about:preferences#general-translations", "tab");
+ }
+
+ /**
+ * Handles events when a command event is triggered within the panel.
+ *
+ * @param {Element} target - The event target
+ */
+ #handleCommandEvent(target) {
+ const {
+ cancelButton,
+ copyButton,
+ doneButtonPrimary,
+ doneButtonSecondary,
+ fromMenuList,
+ fromMenuPopup,
+ settingsButton,
+ toMenuList,
+ toMenuPopup,
+ translateButton,
+ translateFullPageButton,
+ tryAgainButton,
+ tryAnotherSourceMenuList,
+ tryAnotherSourceMenuPopup,
+ } = this.elements;
+ switch (target.id) {
+ case cancelButton.id:
+ case doneButtonPrimary.id:
+ case doneButtonSecondary.id: {
+ this.close();
+ break;
+ }
+ case copyButton.id: {
+ this.onClickCopyButton();
+ break;
+ }
+ case fromMenuList.id:
+ case fromMenuPopup.id: {
+ this.onChangeFromLanguage();
+ break;
+ }
+ case settingsButton.id: {
+ this.#openSettingsPopup();
+ break;
+ }
+ case toMenuList.id:
+ case toMenuPopup.id: {
+ this.onChangeToLanguage();
+ break;
+ }
+ case translateButton.id: {
+ this.onClickTranslateButton();
+ break;
+ }
+ case translateFullPageButton.id: {
+ this.onClickTranslateFullPageButton();
+ break;
+ }
+ case tryAgainButton.id: {
+ this.onClickTryAgainButton();
+ break;
+ }
+ case tryAnotherSourceMenuList.id:
+ case tryAnotherSourceMenuPopup.id: {
+ this.onChangeTryAnotherSourceLanguage();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles events when a popup is shown within the panel, including showing
+ * the panel itself.
+ *
+ * @param {Element} target - The event target
+ */
+ #handlePopupShownEvent(target) {
+ const { panel } = this.elements;
+ switch (target.id) {
+ case panel.id: {
+ this.#updatePanelUIFromState();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles events when a popup is closed within the panel, including closing
+ * the panel itself.
+ *
+ * @param {Element} target - The event target
+ */
+ #handlePopupHiddenEvent(target) {
+ const { panel } = this.elements;
+ switch (target.id) {
+ case panel.id: {
+ this.#changeStateToClosed();
+ this.#removeActiveTranslationListeners();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles events in the SelectTranslationsPanel.
+ *
+ * @param {Event} event - The event to handle.
+ */
+ handleEvent(event) {
+ let target = event.target;
+
+ // If a menuitem within a menulist is the target, those don't have ids,
+ // so we want to traverse until we get to a parent element with an id.
+ while (!target.id && target.parentElement) {
+ target = target.parentElement;
+ }
+
+ switch (event.type) {
+ case "command": {
+ this.#handleCommandEvent(target);
+ break;
+ }
+ case "popupshown": {
+ this.#handlePopupShownEvent(target);
+ break;
+ }
+ case "popuphidden": {
+ this.#handlePopupHiddenEvent(target);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handles events when the panels select from-language is changed.
+ */
+ onChangeFromLanguage() {
+ this.#updateConditionalUIEnabledState();
+ }
+
+ /**
+ * Handles events when the panels select to-language is changed.
+ */
+ onChangeToLanguage() {
+ this.#updateConditionalUIEnabledState();
+ }
+
+ /**
+ * Handles events when the panel's try-another-source language is changed.
+ */
+ onChangeTryAnotherSourceLanguage() {
+ const { tryAnotherSourceMenuList, translateButton } = this.elements;
+ if (tryAnotherSourceMenuList.value) {
+ translateButton.disabled = false;
+ }
+ }
+
+ /**
+ * Handles events when the panel's copy button is clicked.
+ */
+ onClickCopyButton() {
+ try {
+ ClipboardHelper.copyString(this.getTranslatedText());
+ } catch (error) {
+ this.console?.error(error);
+ return;
+ }
+
+ this.#checkCopyButton();
+ }
+
+ /**
+ * Handles events when the panel's translate button is clicked.
+ */
+ onClickTranslateButton() {
+ const { fromMenuList, tryAnotherSourceMenuList } = this.elements;
+ fromMenuList.value = tryAnotherSourceMenuList.value;
+ this.#maybeRequestTranslation();
+ }
+
+ /**
+ * Handles events when the panel's translate-full-page button is clicked.
+ */
+ onClickTranslateFullPageButton() {
+ const { panel } = this.elements;
+ const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair();
+
+ try {
+ const actor = TranslationsParent.getTranslationsActor(
+ gBrowser.selectedBrowser
+ );
+ panel.addEventListener(
+ "popuphidden",
+ () =>
+ actor.translate(
+ fromLanguage,
+ toLanguage,
+ false // reportAsAutoTranslate
+ ),
+ { once: true }
+ );
+ } catch (error) {
+ // This situation would only occur if the translate-full-page button as invoked
+ // while Translations actor is not available. the logic within this class explicitly
+ // hides the button in this case, and this should not be possible under normal conditions,
+ // but if this button were to somehow still be invoked, the best thing we can do here is log
+ // an error to the console because the FullPageTranslationsPanel assumes that the actor is available.
+ this.console?.error(error);
+ }
+
+ this.close();
+ }
+
+ /**
+ * Handles events when the panel's try-again button is clicked.
+ */
+ onClickTryAgainButton() {
+ switch (this.phase()) {
+ case "translation-failure": {
+ // If the translation failed, we just need to try translating again.
+ this.#maybeRequestTranslation();
+ break;
+ }
+ case "init-failure": {
+ // If the initialization failed, we need to close the panel and try reopening it
+ // which will attempt to initialize everything again after failure.
+ const { panel } = this.elements;
+ const { event, screenX, screenY, sourceText, langPairPromise } =
+ this.#translationState;
+
+ panel.addEventListener(
+ "popuphidden",
+ () => this.open(event, screenX, screenY, sourceText, langPairPromise),
+ { once: true }
+ );
+
+ this.close();
+ break;
+ }
+ default: {
+ this.console?.error(
+ `Unexpected state "${this.phase()}" on try-again button click.`
+ );
+ }
+ }
+ }
+
+ /**
+ * Changes the copy button's visual icon to checked, and its localized text to "Copied".
+ */
+ #checkCopyButton() {
+ const { copyButton } = this.elements;
+ copyButton.classList.add("copied");
+ document.l10n.setAttributes(
+ copyButton,
+ "select-translations-panel-copy-button-copied"
+ );
+ }
+
+ /**
+ * Changes the copy button's visual icon to unchecked, and its localized text to "Copy".
+ */
+ #uncheckCopyButton() {
+ const { copyButton } = this.elements;
+ copyButton.classList.remove("copied");
+ document.l10n.setAttributes(
+ copyButton,
+ "select-translations-panel-copy-button"
+ );
+ }
- await this.#ensureLangListsBuilt();
- await this.#updateLanguageDropdowns(langPairPromise);
+ /**
+ * 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]);
+ }
- // 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;
+ /**
+ * 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;
+ }
- panel.addEventListener("popupshown", () => textArea.focus(), {
- once: true,
+ 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;
+ });
});
- await PanelMultiView.openPopup(panel, appMenuButton, {
- position: "bottomright topright",
- triggerEvent: event,
- }).catch(error => this.console?.error(error));
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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 {string}
+ */
+ 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) {
+ switch (phase) {
+ case "closed":
+ case "idle":
+ case "init-failure":
+ case "translation-failure":
+ case "translatable":
+ case "translating":
+ case "translated":
+ case "unsupported": {
+ // Phase is valid, continue on.
+ 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, detectedLanguage } =
+ this.#translationState;
+ const sourceLanguage = fromLanguage ? fromLanguage : detectedLanguage;
+ this.console?.debug(
+ `SelectTranslationsPanel (${sourceLanguage ? sourceLanguage : "??"}-${
+ toLanguage ? toLanguage : "??"
+ }) state change (${previousPhase} => ${phase})`
+ );
+
+ this.#updatePanelUIFromState();
+ document.dispatchEvent(
+ new CustomEvent("SelectTranslationsPanelStateChanged", {
+ detail: { phase },
+ })
+ );
+ }
+
+ /**
+ * 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,
+ });
+ }
+
+ /**
+ * Changes the phase to "init-failure".
+ */
+ #changeStateToInitFailure(
+ event,
+ screenX,
+ screenY,
+ sourceText,
+ langPairPromise
+ ) {
+ this.#changeStateTo("init-failure", /* retainEntries */ true, {
+ event,
+ screenX,
+ screenY,
+ sourceText,
+ langPairPromise,
+ });
+ }
+
+ /**
+ * Changes the phase from "translating" to "translation-failure".
+ */
+ #changeStateToTranslationFailure() {
+ const phase = this.phase();
+ if (phase !== "translating") {
+ this.console?.error(
+ `Invalid state change (${phase} => translation-failure)`
+ );
+ }
+ this.#changeStateTo("translation-failure", /* retainEntries */ true);
+ }
+
+ /**
+ * Transitions the phase to "translatable" if the proper conditions are met,
+ * otherwise retains the same phase as before.
+ *
+ * @param {string} fromLanguage - The BCP-47 from-language tag.
+ * @param {string} toLanguage - The BCP-47 to-language tag.
+ */
+ #maybeChangeStateToTranslatable(fromLanguage, toLanguage) {
+ const {
+ fromLanguage: previousFromLanguage,
+ toLanguage: previousToLanguage,
+ } = this.#translationState;
+
+ const langSelectionChanged = () =>
+ previousFromLanguage !== fromLanguage ||
+ previousToLanguage !== toLanguage;
+
+ const shouldTranslateEvenIfLangSelectionHasNotChanged = () => {
+ const phase = this.phase();
+ return (
+ // The panel has just opened, and this is the initial translation.
+ phase === "idle" ||
+ // The previous translation failed and we are about to try again.
+ phase === "translation-failure"
+ );
+ };
+
+ if (
+ // A valid from-language is actively selected.
+ fromLanguage &&
+ // A valid to-language is actively selected.
+ toLanguage &&
+ // The language selection has changed, requiring a new translation.
+ (langSelectionChanged() ||
+ // We should try to translate even if the language selection has not changed.
+ shouldTranslateEvenIfLangSelectionHasNotChanged())
+ ) {
+ this.#changeStateTo("translatable", /* retainEntries */ true, {
+ fromLanguage,
+ toLanguage,
+ });
+ }
+ }
+
+ /**
+ * Handles changes to the copy button based on the current translation state.
+ *
+ * @param {string} phase - The current phase of the translation state.
+ */
+ #handleCopyButtonChanges(phase) {
+ switch (phase) {
+ case "closed":
+ case "translation-failure":
+ case "translated": {
+ this.#uncheckCopyButton();
+ break;
+ }
+ case "idle":
+ case "init-failure":
+ case "translatable":
+ case "translating":
+ case "unsupported": {
+ // Do nothing.
+ break;
+ }
+ default: {
+ throw new Error(`Invalid state change to '${phase}'`);
+ }
+ }
+ }
+
+ /**
+ * Handles changes to the text area's background image based on the current translation state.
+ *
+ * @param {string} phase - The current phase of the translation state.
+ */
+ #handleTextAreaBackgroundChanges(phase) {
+ const { textArea } = this.elements;
+ switch (phase) {
+ case "translating": {
+ textArea.classList.add("translating");
+ break;
+ }
+ case "closed":
+ case "idle":
+ case "init-failure":
+ case "translation-failure":
+ case "translatable":
+ case "translated":
+ case "unsupported": {
+ textArea.classList.remove("translating");
+ break;
+ }
+ default: {
+ throw new Error(`Invalid state change to '${phase}'`);
+ }
+ }
+ }
+
+ /**
+ * Handles changes to the primary UI components based on the current translation state.
+ *
+ * @param {string} phase - The current phase of the translation state.
+ */
+ #handlePrimaryUIChanges(phase) {
+ switch (phase) {
+ case "closed":
+ case "idle": {
+ this.#displayIdlePlaceholder();
+ break;
+ }
+ case "init-failure": {
+ this.#displayInitFailureMessage();
+ break;
+ }
+ case "translation-failure": {
+ this.#displayTranslationFailureMessage();
+ break;
+ }
+ case "translatable": {
+ // Do nothing.
+ break;
+ }
+ case "translating": {
+ this.#displayTranslatingPlaceholder();
+ break;
+ }
+ case "translated": {
+ this.#displayTranslatedText();
+ break;
+ }
+ case "unsupported": {
+ this.#displayUnsupportedLanguageMessage();
+ break;
+ }
+ default: {
+ throw new Error(`Invalid state change to '${phase}'`);
+ }
+ }
+ }
+
+ /**
+ * 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)
+ );
+ }
+
+ /**
+ * Displays the placeholder text for the translation state's "idle" phase.
+ */
+ #displayIdlePlaceholder() {
+ this.#showMainContent();
+
+ 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() {
+ this.#showMainContent();
+
+ 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() {
+ this.#showMainContent();
+
+ const { toLanguage } = this.#getSelectedLanguagePair();
+ const { textArea } = SelectTranslationsPanel.elements;
+ textArea.value = this.getTranslatedText();
+ this.#updateTextDirection(toLanguage);
+ this.#updateConditionalUIEnabledState();
+ this.#indicateTranslatedTextArea({ overflow: "auto" });
+ }
+
+ /**
+ * Sets attributes on panel elements that are specifically relevant
+ * to the SelectTranslationsPanel's state.
+ *
+ * @param {object} options - Options of which attributes to set.
+ * @param {Record<string, Element[]>} options.makeHidden - Make these elements hidden.
+ * @param {Record<string, Element[]>} options.makeVisible - Make these elements visible.
+ */
+ #setPanelElementAttributes({ makeHidden = [], makeVisible = [] }) {
+ for (const element of makeHidden) {
+ element.hidden = true;
+ }
+ for (const element of makeVisible) {
+ element.hidden = false;
+ }
+ }
+
+ /**
+ * Enables or disables UI components that are conditional on a valid language pair being selected.
+ */
+ #updateConditionalUIEnabledState() {
+ const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair();
+ const {
+ copyButton,
+ textArea,
+ translateButton,
+ translateFullPageButton,
+ tryAnotherSourceMenuList,
+ } = this.elements;
+
+ const invalidLangPairSelected = !fromLanguage || !toLanguage;
+ const isTranslating = this.phase() === "translating";
+
+ textArea.disabled = invalidLangPairSelected;
+ copyButton.disabled = invalidLangPairSelected || isTranslating;
+ translateButton.disabled = !tryAnotherSourceMenuList.value;
+ translateFullPageButton.disabled =
+ invalidLangPairSelected ||
+ fromLanguage === toLanguage ||
+ this.#isFullPageTranslationsRestrictedForPage;
+ }
+
+ /**
+ * Updates the panel UI based on the current phase of the translation state.
+ */
+ #updatePanelUIFromState() {
+ const phase = this.phase();
+ this.#handlePrimaryUIChanges(phase);
+ this.#handleCopyButtonChanges(phase);
+ this.#handleTextAreaBackgroundChanges(phase);
+ }
+
+ /**
+ * Shows the panel's main-content group of elements.
+ */
+ #showMainContent() {
+ const {
+ cancelButton,
+ copyButton,
+ doneButtonPrimary,
+ doneButtonSecondary,
+ initFailureContent,
+ mainContent,
+ unsupportedLanguageContent,
+ textArea,
+ translateButton,
+ translateFullPageButton,
+ translationFailureMessageBar,
+ tryAgainButton,
+ } = this.elements;
+ this.#setPanelElementAttributes({
+ makeHidden: [
+ cancelButton,
+ doneButtonSecondary,
+ initFailureContent,
+ translateButton,
+ translationFailureMessageBar,
+ tryAgainButton,
+ unsupportedLanguageContent,
+ ...(this.#isFullPageTranslationsRestrictedForPage
+ ? [translateFullPageButton]
+ : []),
+ ],
+ makeVisible: [
+ mainContent,
+ copyButton,
+ doneButtonPrimary,
+ textArea,
+ ...(this.#isFullPageTranslationsRestrictedForPage
+ ? []
+ : [translateFullPageButton]),
+ ],
+ });
+ }
+
+ /**
+ * Shows the panel's unsupported-language group of elements.
+ */
+ #showUnsupportedLanguageContent() {
+ const {
+ cancelButton,
+ copyButton,
+ doneButtonPrimary,
+ doneButtonSecondary,
+ initFailureContent,
+ mainContent,
+ unsupportedLanguageContent,
+ translateButton,
+ translateFullPageButton,
+ tryAgainButton,
+ } = this.elements;
+ this.#setPanelElementAttributes({
+ makeHidden: [
+ cancelButton,
+ doneButtonPrimary,
+ copyButton,
+ initFailureContent,
+ mainContent,
+ translateFullPageButton,
+ tryAgainButton,
+ ],
+ makeVisible: [
+ doneButtonSecondary,
+ translateButton,
+ unsupportedLanguageContent,
+ ],
+ });
+ }
+
+ /**
+ * Displays the panel content for when the language dropdowns fail to populate.
+ */
+ #displayInitFailureMessage() {
+ const {
+ cancelButton,
+ copyButton,
+ doneButtonPrimary,
+ doneButtonSecondary,
+ initFailureContent,
+ mainContent,
+ unsupportedLanguageContent,
+ translateButton,
+ translateFullPageButton,
+ tryAgainButton,
+ } = this.elements;
+ this.#setPanelElementAttributes({
+ makeHidden: [
+ doneButtonPrimary,
+ doneButtonSecondary,
+ copyButton,
+ mainContent,
+ translateButton,
+ translateFullPageButton,
+ unsupportedLanguageContent,
+ ],
+ makeVisible: [initFailureContent, cancelButton, tryAgainButton],
+ });
+ tryAgainButton.focus({ focusVisible: true });
+ }
+
+ /**
+ * Displays the panel content for when a translation fails to complete.
+ */
+ #displayTranslationFailureMessage() {
+ const {
+ cancelButton,
+ copyButton,
+ doneButtonPrimary,
+ doneButtonSecondary,
+ initFailureContent,
+ mainContent,
+ textArea,
+ translateButton,
+ translateFullPageButton,
+ translationFailureMessageBar,
+ tryAgainButton,
+ unsupportedLanguageContent,
+ } = this.elements;
+ this.#setPanelElementAttributes({
+ makeHidden: [
+ doneButtonPrimary,
+ doneButtonSecondary,
+ copyButton,
+ initFailureContent,
+ translateButton,
+ translateFullPageButton,
+ textArea,
+ unsupportedLanguageContent,
+ ],
+ makeVisible: [
+ cancelButton,
+ mainContent,
+ translationFailureMessageBar,
+ tryAgainButton,
+ ],
+ });
+ tryAgainButton.focus({ focusVisible: true });
+ }
+
+ /**
+ * Displays the panel's unsupported language message bar, showing
+ * the panel's unsupported-language elements.
+ */
+ #displayUnsupportedLanguageMessage() {
+ const { detectedLanguage } = this.#translationState;
+ const { unsupportedLanguageMessageBar, tryAnotherSourceMenuList } =
+ this.elements;
+ const displayNames = new Services.intl.DisplayNames(undefined, {
+ type: "language",
+ });
+ try {
+ const language = displayNames.of(detectedLanguage);
+ if (language) {
+ document.l10n.setAttributes(
+ unsupportedLanguageMessageBar,
+ "select-translations-panel-unsupported-language-message-known",
+ { language }
+ );
+ } else {
+ // Will be immediately caught.
+ throw new Error();
+ }
+ } catch {
+ // Either displayNames.of() threw, or we threw due to no display name found.
+ // In either case, localize the message for an unknown language.
+ document.l10n.setAttributes(
+ unsupportedLanguageMessageBar,
+ "select-translations-panel-unsupported-language-message-unknown"
+ );
+ }
+ this.#updateConditionalUIEnabledState();
+ this.#showUnsupportedLanguageContent();
+ this.#maybeFocusMenuList(tryAnotherSourceMenuList);
+ }
+
+ /**
+ * 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 #createTranslator(fromLanguage, toLanguage) {
+ this.console?.log(
+ `Creating new Translator (${fromLanguage}-${toLanguage})`
+ );
+
+ const translator = await Translator.create(fromLanguage, toLanguage, {
+ allowSameLanguage: true,
+ requestTranslationsPort: this.#requestTranslationsPort,
+ });
+ return 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();
+ this.#maybeChangeStateToTranslatable(fromLanguage, toLanguage);
+
+ if (this.phase() !== "translatable") {
+ return;
+ }
+
+ const translationId = ++this.#translationId;
+ this.#createTranslator(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);
+ }
+ })
+ .catch(error => {
+ this.console?.error(error);
+ this.#changeStateToTranslationFailure();
+ });
+ }
+
+ /**
+ * 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 "focus":
+ case "popuphidden": {
+ callback = () => {
+ this.#maybeRequestTranslation();
+ };
+ break;
+ }
+ case "keypress": {
+ callback = event => {
+ if (event.key === "Enter") {
+ this.#maybeRequestTranslation();
+ }
+ };
+ break;
+ }
+ default: {
+ throw new Error(
+ `Invalid translation event type given: '${eventType}`
+ );
+ }
+ }
+ target.addEventListener(eventType, callback);
+ target.translationListenerCallbacks.push({ eventType, callback });
+ }
+ }
+ }
+
+ /**
+ * Removes all translation event listeners from any panel elements that would have one.
+ */
+ #removeActiveTranslationListeners() {
+ const { fromMenuList, fromMenuPopup, textArea, toMenuList, toMenuPopup } =
+ SelectTranslationsPanel.elements;
+ this.#removeTranslationListenersFrom(fromMenuList);
+ this.#removeTranslationListenersFrom(fromMenuPopup);
+ this.#removeTranslationListenersFrom(textArea);
+ this.#removeTranslationListenersFrom(toMenuList);
+ this.#removeTranslationListenersFrom(toMenuPopup);
+ }
+
+ /**
+ * Removes all translation event listeners from the target element.
+ *
+ * @param {Element} target - The element from which event listeners are to be removed.
+ */
+ #removeTranslationListenersFrom(target) {
+ if (!target.translationListenerCallbacks) {
+ return;
+ }
+
+ 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..78ffd04c09 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"]
@@ -99,6 +105,8 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227
["browser_translations_full_page_telemetry_translation_request.js"]
+["browser_translations_select_context_menu_engine_unsupported.js"]
+
["browser_translations_select_context_menu_feature_disabled.js"]
["browser_translations_select_context_menu_with_full_page_translations_active.js"]
@@ -109,6 +117,50 @@ 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_close_on_new_tab.js"]
+
+["browser_translations_select_panel_copy_button.js"]
+
+["browser_translations_select_panel_engine_cache.js"]
+
+["browser_translations_select_panel_fallback_to_doc_language.js"]
+
+["browser_translations_select_panel_init_failure.js"]
+
+["browser_translations_select_panel_pdf.js"]
+
+["browser_translations_select_panel_reader_mode.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_settings_menu.js"]
+
+["browser_translations_select_panel_translate_full_page_button.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_translate_on_open.js"]
+
+["browser_translations_select_panel_translation_failure_after_unsupported_language.js"]
+
+["browser_translations_select_panel_translation_failure_on_open.js"]
+
+["browser_translations_select_panel_translation_failure_on_retranslate.js"]
-["browser_translations_select_panel_mainview_ui.js"]
+["browser_translations_select_panel_unsupported_language.js"]
diff --git a/browser/components/translations/tests/browser/browser_translations_about_preferences_manage_downloaded_languages.js b/browser/components/translations/tests/browser/browser_translations_about_preferences_manage_downloaded_languages.js
index 383f2094a7..6d5b10f26c 100644
--- a/browser/components/translations/tests/browser/browser_translations_about_preferences_manage_downloaded_languages.js
+++ b/browser/components/translations/tests/browser/browser_translations_about_preferences_manage_downloaded_languages.js
@@ -36,7 +36,7 @@ add_task(async function test_about_preferences_manage_languages() {
is(
downloadAllLabel.getAttribute("data-l10n-id"),
- "translations-manage-install-description",
+ "translations-manage-download-description",
"The first row is all of the languages."
);
is(frenchLabel.textContent, "French", "There is a French row.");
@@ -178,7 +178,7 @@ add_task(async function test_about_preferences_download_reject() {
click(frenchDownload, "Downloading French");
is(
- maybeGetByL10nId("translations-manage-error-install", document),
+ maybeGetByL10nId("translations-manage-error-download", document),
null,
"No error messages are present."
);
@@ -200,13 +200,13 @@ add_task(async function test_about_preferences_download_reject() {
}
await waitForCondition(
- () => maybeGetByL10nId("translations-manage-error-install", document),
+ () => maybeGetByL10nId("translations-manage-error-download", document),
"The error message is now visible."
);
click(frenchDownload, "Attempting to download French again", document);
is(
- maybeGetByL10nId("translations-manage-error-install", document),
+ maybeGetByL10nId("translations-manage-error-download", document),
null,
"The error message is hidden again."
);
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..39495a823c 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,199 @@ 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.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").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").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_engine_unsupported.js b/browser/components/translations/tests/browser/browser_translations_select_context_menu_engine_unsupported.js
new file mode 100644
index 0000000000..b5fe9de0ef
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_context_menu_engine_unsupported.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test checks the availability of the translate-selection menu item in the context menu,
+ * ensuring it is not visible when the hardware does not support Translations. In this case
+ * we simulate this scenario by setting "browser.translations.simulateUnsupportedEngine" to true.
+ */
+add_task(
+ async function test_translate_selection_menuitem_is_unavailable_when_engine_is_unsupported() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [
+ ["browser.translations.enable", true],
+ ["browser.translations.select.enable", true],
+ ["browser.translations.simulateUnsupportedEngine", true],
+ ],
+ });
+
+ await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
+ runInPage,
+ {
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
+ expectMenuItemVisible: false,
+ },
+ "The translate-selection context menu item should be unavailable the translations engine is unsupported."
+ );
+
+ await cleanup();
+ }
+);
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..cb0f3601d9 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",
@@ -41,13 +41,13 @@ add_task(
/**
* This test case verifies the functionality of the translate-selection context menu item
* when a hyperlink is right-clicked, and the link text is in the top preferred language.
- * The menu item should offer to translate the link text without specifying a target language,
- * since it is already in the preferred language for the user.
+ * The menu item should still offer to translate the link text to the top preferred language,
+ * since the Select Translations Panel should pass through the text for same-language translation.
*/
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,13 +60,13 @@ add_task(
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
{
- selectSpanishParagraph: false,
+ selectSpanishSentence: false,
openAtEnglishHyperlink: true,
expectMenuItemVisible: true,
- expectedTargetLanguage: null,
+ expectedTargetLanguage: "en",
},
"The translate-selection context menu item should be localized to translate the link text" +
- "without a target language."
+ "to the target language."
);
await cleanup();
@@ -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..562eef3efb 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",
},
@@ -43,31 +43,31 @@ add_task(
/**
* This test case verifies the functionality of the translate-selection context menu item
* when the selected text is detected to be in the user's preferred language. The menu item
- * should not be localized to display a target language when the selected text matches the
- * user's top preferred language.
+ * still be localized to the user's preferred language as a target, since the Select Translations
+ * Panel allows passing through the text for same-language translation.
*/
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,
+ expectedTargetLanguage: "en",
},
- "The translate-selection context menu item should not display a target language " +
+ "The translate-selection context menu item should still display a target language " +
"when the selected text is in the preferred language."
);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_close_on_new_tab.js b/browser/components/translations/tests/browser/browser_translations_select_panel_close_on_new_tab.js
new file mode 100644
index 0000000000..86e563b157
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_close_on_new_tab.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests the scenario where the SelectTranslationsPanel is open
+ * and the user opens a new tab while the panel is still open. The panel should
+ * close appropriately, as the content relevant to the selection is no longer
+ * in the active tab.
+ */
+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,
+ });
+
+ let tab;
+
+ await SelectTranslationsTestUtils.waitForPanelPopupEvent(
+ "popuphidden",
+ async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SPANISH_PAGE_URL,
+ true // waitForLoad
+ );
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_copy_button.js b/browser/components/translations/tests/browser/browser_translations_select_panel_copy_button.js
new file mode 100644
index 0000000000..29eafb980d
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_copy_button.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests functionality of the SelectTranslationsPanel copy button
+ * when retranslating by closing the panel and re-opening the panel to new links
+ * or selections of text.
+ */
+add_task(async function test_select_translations_panel_copy_button_on_reopen() {
+ 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.clickCopyButton();
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectEnglishSection: true,
+ openAtEnglishSection: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: "en",
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickCopyButton();
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSection: true,
+ openAtFrenchSection: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickCopyButton();
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+});
+
+/**
+ * This test case tests functionality of the SelectTranslationsPanel copy button
+ * when retranslating by changing the from-language and to-language values for
+ * the same selection of source text.
+ */
+add_task(
+ async function test_select_translations_panel_copy_button_on_retranslate() {
+ 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.clickCopyButton();
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickCopyButton();
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], {
+ openDropdownMenu: false,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickCopyButton();
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
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_init_failure.js b/browser/components/translations/tests/browser/browser_translations_select_panel_init_failure.js
new file mode 100644
index 0000000000..9e17b0705f
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_init_failure.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the scenario of clicking the cancel button to close
+ * the SelectTranslationsPanel after the language lists fail to initialize upon
+ * opening the panel, and the proper error message is displayed.
+ */
+add_task(async function test_select_translations_panel_init_failure_cancel() {
+ const { cleanup, runInPage } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ TranslationsPanelShared.simulateLangListError();
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSentence: true,
+ openAtFrenchSentence: true,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewInitFailure,
+ });
+
+ await SelectTranslationsTestUtils.clickCancelButton();
+
+ await cleanup();
+});
+
+/**
+ * This test case verifies the scenario of opening the SelectTranslationsPanel to a valid
+ * language pair, but having the language lists fail to initialize, then clicking the try-again
+ * button multiple times until both initialization and translation succeed.
+ */
+add_task(
+ async function test_select_translations_panel_init_failure_try_again_into_translation() {
+ const { cleanup, runInPage, resolveDownloads, rejectDownloads } =
+ await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ TranslationsPanelShared.simulateLangListError();
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectFrenchSentence: true,
+ openAtFrenchSentence: true,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewInitFailure,
+ });
+
+ TranslationsPanelShared.simulateLangListError();
+ await SelectTranslationsTestUtils.waitForPanelPopupEvent(
+ "popupshown",
+ SelectTranslationsTestUtils.clickTryAgainButton,
+ SelectTranslationsTestUtils.assertPanelViewInitFailure
+ );
+
+ await SelectTranslationsTestUtils.waitForPanelPopupEvent(
+ "popupshown",
+ async () =>
+ SelectTranslationsTestUtils.clickTryAgainButton({
+ downloadHandler: rejectDownloads,
+ }),
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure
+ );
+
+ await SelectTranslationsTestUtils.clickTryAgainButton({
+ downloadHandler: resolveDownloads,
+ viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the scenario of opening the SelectTranslationsPanel to an unsupported
+ * language, but having the language lists fail to initialize, then clicking the try-again
+ * button multiple times until the unsupported-language view is shown.
+ */
+add_task(
+ async function test_select_translations_panel_init_failure_try_again_into_unsupported() {
+ 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]],
+ });
+
+ TranslationsPanelShared.simulateLangListError();
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewInitFailure,
+ });
+
+ TranslationsPanelShared.simulateLangListError();
+ await SelectTranslationsTestUtils.waitForPanelPopupEvent(
+ "popupshown",
+ SelectTranslationsTestUtils.clickTryAgainButton,
+ SelectTranslationsTestUtils.assertPanelViewInitFailure
+ );
+
+ await SelectTranslationsTestUtils.waitForPanelPopupEvent(
+ "popupshown",
+ SelectTranslationsTestUtils.clickTryAgainButton,
+ SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage
+ );
+
+ 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_pdf.js b/browser/components/translations/tests/browser/browser_translations_select_panel_pdf.js
new file mode 100644
index 0000000000..fd675e9cea
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_pdf.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies that the Select Translations Panel functionality
+ * is available and works within PDF files.
+ */
+add_task(async function test_the_select_translations_panel_in_pdf_files() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: PDF_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectPdfSpan: true,
+ openAtPdfSpan: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: "en",
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], {
+ openDropdownMenu: true,
+ pivotTranslation: false,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["fr"], {
+ openDropdownMenu: false,
+ pivotTranslation: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+});
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_reader_mode.js b/browser/components/translations/tests/browser/browser_translations_select_panel_reader_mode.js
new file mode 100644
index 0000000000..5f05b1a878
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_reader_mode.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies that the Select Translations Panel functionality
+ * is available and works within reader mode.
+ */
+add_task(async function test_the_select_translations_panel_in_reader_mode() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await toggleReaderMode();
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectH1: true,
+ openAtH1: true,
+ expectedFromLanguage: "en",
+ expectedToLanguage: "en",
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], {
+ openDropdownMenu: true,
+ pivotTranslation: false,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["fr"], {
+ openDropdownMenu: false,
+ pivotTranslation: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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..673faee796
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_select_same_from_and_to_languages_directly.js
@@ -0,0 +1,66 @@
+/* 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, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["en"], {
+ openDropdownMenu: false,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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, creating a passthrough translation
+ * of the source text directly into the text area.
+ */
+add_task(
+ async function test_select_translations_panel_select_same_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(["fr"], {
+ openDropdownMenu: false,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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..eea7a76bf2
--- /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,66 @@
+/* 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, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["en"], {
+ openDropdownMenu: true,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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,
+ * creating a passthrough translation of the source text directly into the text area.
+ */
+add_task(
+ async function test_select_translations_panel_select_same_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, {
+ selectFrenchSentence: true,
+ openAtFrenchSentence: true,
+ expectedFromLanguage: "fr",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["fr"], {
+ openDropdownMenu: true,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_settings_menu.js b/browser/components/translations/tests/browser/browser_translations_select_panel_settings_menu.js
new file mode 100644
index 0000000000..b6263325d5
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_settings_menu.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests the scenario of clicking the settings menu item
+ * that leads to the translations section of the about:preferences settings
+ * page in Firefox.
+ */
+add_task(async function test_select_translations_panel_open_settings_page() {
+ 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.openPanelSettingsMenu();
+ SelectTranslationsTestUtils.clickTranslationsSettingsPageMenuItem();
+
+ await waitForCondition(
+ () => gBrowser.currentURI.spec === "about:preferences#general",
+ "Waiting for about:preferences to be opened."
+ );
+
+ info("Remove the about:preferences tab");
+ gBrowser.removeCurrentTab();
+
+ await cleanup();
+});
+
+/**
+ * This test case tests the scenario of opening the SelectTranslationsPanel
+ * settings menu from the unsupported-language panel state.
+ */
+add_task(
+ async function test_select_translations_panel_open_settings_menu_from_unsupported_language() {
+ 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,
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ await SelectTranslationsTestUtils.openPanelSettingsMenu();
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translate_full_page_button.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_full_page_button.js
new file mode 100644
index 0000000000..a2e9727798
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_full_page_button.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Simulates clicking the translate-full-page button with a from-language that
+ * matches the language of the given document.
+ */
+add_task(
+ async function test_select_translations_panel_translat_full_page_button_matching_doc_lang() {
+ 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.clickTranslateFullPageButton();
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "es",
+ "en",
+ runInPage
+ );
+
+ await cleanup();
+ }
+);
+
+/**
+ * Simulates clicking the translate-full-page button after changing the from-language
+ * and to-language values to values that don't match the document language or the
+ * user's app locale, ensuring that the current selection is respected.
+ */
+add_task(
+ async function test_select_translations_panel_translat_full_page_button_matching_doc_lang() {
+ const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
+ page: SELECT_TEST_PAGE_URL,
+ languagePairs: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["fr"], {
+ openDropdownMenu: false,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ pivotTranslation: true,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickTranslateFullPageButton();
+
+ await FullPageTranslationsTestUtils.assertPageIsTranslated(
+ "fr",
+ "uk",
+ runInPage
+ );
+
+ 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..7ea721fb0f
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_directly.js
@@ -0,0 +1,67 @@
+/* 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: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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: "en",
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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..c0ba02f6db
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js
@@ -0,0 +1,67 @@
+/* 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: LANGUAGE_PAIRS,
+ prefs: [["browser.translations.select.enable", true]],
+ });
+
+ await SelectTranslationsTestUtils.openPanel(runInPage, {
+ selectSpanishSection: true,
+ openAtSpanishSection: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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: "en",
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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..cd60f73e6c
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translate_on_change_language_multiple_times_directly.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,
+ * 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: [
+ { 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, {
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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: "en",
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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..1b2044ef97
--- /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,99 @@
+/* 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: [
+ { 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, {
+ selectSpanishSentence: true,
+ openAtSpanishSentence: true,
+ expectedFromLanguage: "es",
+ expectedToLanguage: "en",
+ downloadHandler: resolveDownloads,
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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: "en",
+ onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ 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/browser_translations_select_panel_translation_failure_after_unsupported_language.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_after_unsupported_language.js
new file mode 100644
index 0000000000..79ce46b7dc
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_after_unsupported_language.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests the scenario of encountering the translation failure message
+ * as a result of changing the source language from the unsupported-language state.
+ */
+add_task(
+ async function test_select_translations_panel_failure_after_unsupported_language() {
+ const { cleanup, runInPage, resolveDownloads, rejectDownloads } =
+ 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,
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedTryAnotherSourceLanguage(
+ "fr"
+ );
+
+ await SelectTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: rejectDownloads,
+ viewAssertion:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.clickTryAgainButton({
+ downloadHandler: rejectDownloads,
+ viewAssertion:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.clickTryAgainButton({
+ downloadHandler: resolveDownloads,
+ viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_open.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_open.js
new file mode 100644
index 0000000000..0fc133f269
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_open.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests the scenario of opening the SelectTranslationsPanel to a translation
+ * attempt that fails, followed by closing the panel via the cancel button, and then re-attempting
+ * the translation by re-opening the panel and having it succeed.
+ */
+add_task(
+ async function test_select_translations_panel_translation_failure_on_open_then_cancel_and_reopen() {
+ const { cleanup, runInPage, rejectDownloads, 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: rejectDownloads,
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.clickCancelButton();
+
+ 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 scenario of opening the SelectTranslationsPanel to a translation
+ * attempt that fails, followed by clicking the try-again button multiple times to retry the
+ * translation until it finally succeeds.
+ */
+add_task(
+ async function test_select_translations_panel_translation_failure_on_open_then_try_again() {
+ const { cleanup, runInPage, rejectDownloads, 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: rejectDownloads,
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.clickTryAgainButton({
+ downloadHandler: rejectDownloads,
+ viewAssertion:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.clickTryAgainButton({
+ downloadHandler: rejectDownloads,
+ viewAssertion:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.clickTryAgainButton({
+ downloadHandler: resolveDownloads,
+ viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_retranslate.js b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_retranslate.js
new file mode 100644
index 0000000000..d19439c6a4
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_translation_failure_on_retranslate.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case tests the scenario of encountering the translation failure message
+ * as a result of changing the selected from-language, along with moving from the failure
+ * state to a successful translation also by changing the selected from-language.
+ */
+add_task(
+ async function test_select_translations_panel_translation_failure_on_change_from_language() {
+ const { cleanup, runInPage, rejectDownloads, 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,
+ downloadHandler: rejectDownloads,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], {
+ openDropdownMenu: true,
+ downloadHandler: rejectDownloads,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], {
+ openDropdownMenu: false,
+ downloadHandler: rejectDownloads,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.clickTryAgainButton({
+ downloadHandler: resolveDownloads,
+ viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case tests the scenario of encountering the translation failure message
+ * as a result of changing the selected to-language, along with moving from the failure
+ * state to a successful translation also by changing the selected to-language.
+ */
+add_task(
+ async function test_select_translations_panel_translation_failure_on_change_to_language() {
+ const { cleanup, runInPage, rejectDownloads, 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,
+ downloadHandler: rejectDownloads,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], {
+ openDropdownMenu: true,
+ downloadHandler: rejectDownloads,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ pivotTranslation: true,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], {
+ openDropdownMenu: false,
+ downloadHandler: rejectDownloads,
+ onChangeLanguage:
+ SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
+ });
+
+ await SelectTranslationsTestUtils.clickTryAgainButton({
+ downloadHandler: resolveDownloads,
+ pivotTranslation: true,
+ viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
diff --git a/browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js b/browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js
new file mode 100644
index 0000000000..0898bd125b
--- /dev/null
+++ b/browser/components/translations/tests/browser/browser_translations_select_panel_unsupported_language.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This test case verifies the behavior of opening the SelectTranslationsPanel to an unsupported language
+ * and then clicking the done button to close the panel.
+ */
+add_task(
+ async function test_select_translations_panel_unsupported_click_done_button() {
+ 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,
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of opening the SelectTranslationsPanel to an unsupported language
+ * then changing the source language to the same language as the app locale, triggering a same-language
+ * translation, then changing the from-language and to-language multiple times.
+ */
+add_task(
+ async function test_select_translations_panel_unsupported_then_to_same_language_translation() {
+ 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,
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedTryAnotherSourceLanguage(
+ "en"
+ );
+
+ await SelectTranslationsTestUtils.clickTranslateButton({
+ viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk", "fi"], {
+ openDropdownMenu: false,
+ downloadHandler: resolveDownloads,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["sl", "fr"], {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ pivotTranslation: true,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["fr"], {
+ openDropdownMenu: true,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.clickDoneButton();
+
+ await cleanup();
+ }
+);
+
+/**
+ * This test case verifies the behavior of opening the SelectTranslationsPanel to an unsupported language
+ * then changing the source language to a valid language, followed by changing the from-language and to-language
+ * multiple times.
+ */
+add_task(
+ async function test_select_translations_panel_unsupported_into_different_language_translation() {
+ 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,
+ onOpenPanel:
+ SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedTryAnotherSourceLanguage(
+ "fr"
+ );
+
+ await SelectTranslationsTestUtils.clickTranslateButton({
+ downloadHandler: resolveDownloads,
+ viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk", "fi"], {
+ openDropdownMenu: false,
+ downloadHandler: resolveDownloads,
+ pivotTranslation: true,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedToLanguage(["sl", "uk"], {
+ openDropdownMenu: true,
+ downloadHandler: resolveDownloads,
+ pivotTranslation: true,
+ onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
+ });
+
+ await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], {
+ openDropdownMenu: false,
+ onChangeLanguage: 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..4bd5dc074f 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,33 @@ 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 given element has the expected L10nId.
+ *
+ * @param {Element} element - The element to assert against.
+ * @param {string} l10nId - The expected localization id.
+ */
+ static _assertL10nId(element, l10nId) {
+ is(
+ element.getAttribute("data-l10n-id"),
+ l10nId,
+ `The element ${element.id} should have L10n Id ${l10nId}.`
+ );
+ }
+
+ /**
* Asserts that the mainViewId of the panel matches the given string.
*
* @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
@@ -300,53 +326,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}'`
);
}
}
@@ -380,6 +383,28 @@ class SharedTranslationsTestUtils {
}
/**
+ * Asserts that the given elements are focusable in order
+ * via the tab key, starting with the first element already
+ * focused and ending back on that same first element.
+ *
+ * @param {Element[]} elements - The focusable elements.
+ */
+ static _assertTabIndexOrder(elements) {
+ const activeElementAtStart = document.activeElement;
+
+ if (elements.length) {
+ elements[0].focus();
+ elements.push(elements[0]);
+ }
+ for (const element of elements) {
+ SharedTranslationsTestUtils._assertHasFocus(element);
+ EventUtils.synthesizeKey("KEY_Tab");
+ }
+
+ activeElementAtStart.focus();
+ }
+
+ /**
* Executes the provided callback before waiting for the event and then waits for the given event
* to be fired for the element corresponding to the provided elementId.
*
@@ -391,6 +416,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 +424,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 +597,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 +610,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 +637,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 +659,10 @@ class FullPageTranslationsTestUtils {
);
};
await runInPage(callback, { fromLang: fromLanguage, toLang: toLanguage });
- await FullPageTranslationsTestUtils.#assertLangTagIsShownOnTranslationsButton(
+ await FullPageTranslationsTestUtils.assertLangTagIsShownOnTranslationsButton(
fromLanguage,
- toLanguage
+ toLanguage,
+ win
);
}
@@ -668,6 +703,9 @@ class FullPageTranslationsTestUtils {
changeSourceLanguageButton: false,
dismissErrorButton: false,
error: false,
+ errorMessage: false,
+ errorMessageHint: false,
+ errorHintAction: false,
fromMenuList: false,
fromLabel: false,
header: false,
@@ -694,11 +732,7 @@ class FullPageTranslationsTestUtils {
*/
static #assertPanelHeaderL10nId(l10nId) {
const { header } = FullPageTranslationsPanel.elements;
- is(
- header.getAttribute("data-l10n-id"),
- l10nId,
- "The translations panel header should match the expected data-l10n-id"
- );
+ SharedTranslationsTestUtils._assertL10nId(header, l10nId);
}
/**
@@ -708,11 +742,7 @@ class FullPageTranslationsTestUtils {
*/
static #assertPanelErrorL10nId(l10nId) {
const { errorMessage } = FullPageTranslationsPanel.elements;
- is(
- errorMessage.getAttribute("data-l10n-id"),
- l10nId,
- "The translations panel error message should match the expected data-l10n-id"
- );
+ SharedTranslationsTestUtils._assertL10nId(errorMessage, l10nId);
}
/**
@@ -744,6 +774,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 +811,7 @@ class FullPageTranslationsTestUtils {
);
FullPageTranslationsTestUtils.#assertPanelElementVisibility({
error: true,
+ errorMessage: true,
...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
});
FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
@@ -854,25 +913,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 +945,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)) {
@@ -1069,7 +1139,7 @@ class FullPageTranslationsTestUtils {
static async #clickSettingsMenuItemByL10nId(l10nId) {
info(`Toggling the "${l10nId}" settings menu item.`);
click(getByL10nId(l10nId), `Clicking the "${l10nId}" settings menu item.`);
- await closeSettingsMenuIfOpen();
+ await closeFullPagePanelSettingsMenuIfOpen();
}
/**
@@ -1083,25 +1153,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 +1193,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 +1227,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 +1275,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 +1298,8 @@ class FullPageTranslationsTestUtils {
click(button, "Opening the popup");
}
},
- onOpenPanel
+ onOpenPanel,
+ win
);
}
@@ -1242,7 +1332,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 +1344,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 +1360,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 +1390,62 @@ 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.
+ *
+ * @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.
+ *
+ * The following options will work on all test pages that have an <h1> element.
+ *
+ * @param {boolean} options.selectH1 - Selects the first H1 element of the page.
+ * @param {boolean} options.openAtH1 - Opens the context menu at the first H1 element of the page.
+ *
+ * The following options will work only in the PDF_TEST_PAGE_URL.
+ *
+ * @param {boolean} options.selectPdfSpan - Selects the first span of text on the first page of a pdf.
+ * @param {boolean} options.openAtPdfSpan - Opens the context menu at the first span of text on the first page of a pdf.
+ *
+ * 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.
* @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,
+ selectH1,
+ selectPdfSpan,
+ selectFrenchSection,
+ selectEnglishSection,
+ selectSpanishSection,
+ selectFrenchSentence,
+ selectEnglishSentence,
+ selectSpanishSentence,
+ openAtH1,
+ openAtPdfSpan,
+ openAtFrenchSection,
+ openAtEnglishSection,
+ openAtSpanishSection,
+ openAtFrenchSentence,
+ openAtEnglishSentence,
+ openAtSpanishSentence,
+ openAtFrenchHyperlink,
openAtEnglishHyperlink,
openAtSpanishHyperlink,
},
@@ -1336,10 +1460,25 @@ class SelectTranslationsTestUtils {
await closeAllOpenPanelsAndMenus();
await SelectTranslationsTestUtils.openContextMenu(runInPage, {
- selectFirstParagraph,
- selectSpanishParagraph,
- openAtFirstParagraph,
- openAtSpanishParagraph,
+ expectMenuItemVisible,
+ expectedTargetLanguage,
+ selectH1,
+ selectPdfSpan,
+ selectFrenchSection,
+ selectEnglishSection,
+ selectSpanishSection,
+ selectFrenchSentence,
+ selectEnglishSentence,
+ selectSpanishSentence,
+ openAtH1,
+ openAtPdfSpan,
+ openAtFrenchSection,
+ openAtEnglishSection,
+ openAtSpanishSection,
+ openAtFrenchSentence,
+ openAtEnglishSentence,
+ openAtSpanishSentence,
+ openAtFrenchHyperlink,
openAtEnglishHyperlink,
openAtSpanishHyperlink,
});
@@ -1349,16 +1488,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 +1525,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(
@@ -1410,15 +1559,30 @@ class SelectTranslationsTestUtils {
SelectTranslationsPanel.elements,
{
betaIcon: false,
+ cancelButton: false,
copyButton: false,
- doneButton: false,
+ doneButtonPrimary: false,
+ doneButtonSecondary: false,
fromLabel: false,
fromMenuList: false,
+ fromMenuPopup: false,
header: false,
+ initFailureContent: false,
+ initFailureMessageBar: false,
+ mainContent: false,
+ settingsButton: false,
textArea: false,
toLabel: false,
toMenuList: false,
+ toMenuPopup: false,
+ translateButton: false,
translateFullPageButton: false,
+ translationFailureMessageBar: false,
+ tryAgainButton: false,
+ tryAnotherSourceMenuList: false,
+ tryAnotherSourceMenuPopup: false,
+ unsupportedLanguageContent: false,
+ unsupportedLanguageMessageBar: false,
// Overwrite any of the above defaults with the passed in expectations.
...expectations,
}
@@ -1426,37 +1590,400 @@ class SelectTranslationsTestUtils {
}
/**
- * Asserts that the mainViewId of the panel matches the given string.
+ * Waits for the panel's translation state to reach the given phase,
+ * if it is not currently in that phase already.
*
- * @param {string} expectedId
+ * @param {string} phase - The phase of the panel's translation state to wait for.
*/
- static #assertPanelMainViewId(expectedId) {
- SharedTranslationsTestUtils._assertPanelMainViewId(
- SelectTranslationsPanel,
- expectedId
+ static async waitForPanelState(phase) {
+ const currentPhase = SelectTranslationsPanel.phase();
+ if (currentPhase !== phase) {
+ info(
+ `Waiting for SelectTranslationsPanel to change state from "${currentPhase}" to "${phase}"`
+ );
+ await BrowserTestUtils.waitForEvent(
+ document,
+ "SelectTranslationsPanelStateChanged",
+ event => event.detail.phase === phase
+ );
+ }
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel UI matches the expected
+ * state when the panel has completed its translation.
+ */
+ static async assertPanelViewTranslated() {
+ const {
+ copyButton,
+ doneButtonPrimary,
+ fromMenuList,
+ settingsButton,
+ textArea,
+ toMenuList,
+ translateFullPageButton,
+ } = SelectTranslationsPanel.elements;
+ const sameLanguageSelected = fromMenuList.value === toMenuList.value;
+ await SelectTranslationsTestUtils.waitForPanelState("translated");
+ ok(
+ !textArea.classList.contains("translating"),
+ "The textarea should not have the translating class."
);
+ const isFullPageTranslationsRestrictedForPage =
+ TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser);
+ SelectTranslationsTestUtils.#assertPanelElementVisibility({
+ betaIcon: true,
+ copyButton: true,
+ doneButtonPrimary: true,
+ fromLabel: true,
+ fromMenuList: true,
+ header: true,
+ mainContent: true,
+ settingsButton: true,
+ textArea: true,
+ toLabel: true,
+ toMenuList: true,
+ translateFullPageButton: !isFullPageTranslationsRestrictedForPage,
+ });
+ SelectTranslationsTestUtils.#assertConditionalUIEnabled({
+ copyButton: true,
+ doneButtonPrimary: true,
+ textArea: true,
+ translateFullPageButton:
+ !sameLanguageSelected && !isFullPageTranslationsRestrictedForPage,
+ });
+
+ await waitForCondition(
+ () =>
+ !copyButton.classList.contains("copied") &&
+ copyButton.getAttribute("data-l10n-id") ===
+ "select-translations-panel-copy-button",
+ "Waiting for copy button to match the not-copied state."
+ );
+
+ SelectTranslationsTestUtils.#assertPanelHasTranslatedText();
+ SelectTranslationsTestUtils.#assertPanelTextAreaHeight();
+ await SelectTranslationsTestUtils.#assertPanelTextAreaOverflow();
+
+ let footerButtons;
+ if (sameLanguageSelected || isFullPageTranslationsRestrictedForPage) {
+ footerButtons = [copyButton, doneButtonPrimary];
+ } else {
+ footerButtons =
+ AppConstants.platform === "win"
+ ? [copyButton, doneButtonPrimary, translateFullPageButton]
+ : [copyButton, translateFullPageButton, doneButtonPrimary];
+ }
+
+ SharedTranslationsTestUtils._assertTabIndexOrder([
+ settingsButton,
+ fromMenuList,
+ toMenuList,
+ textArea,
+ ...footerButtons,
+ ]);
}
/**
- * Asserts that panel element visibility matches the default panel view.
+ * Asserts that the SelectTranslationsPanel UI matches the expected
+ * state when the language lists fail to initialize upon opening the panel.
*/
- static assertPanelViewDefault() {
- info("Checking that the select-translations panel shows the default view");
- SelectTranslationsTestUtils.#assertPanelMainViewId(
- "select-translations-panel-view-default"
+ static async assertPanelViewInitFailure() {
+ const { cancelButton, settingsButton, tryAgainButton } =
+ SelectTranslationsPanel.elements;
+ await SelectTranslationsTestUtils.waitForPanelState("init-failure");
+ SelectTranslationsTestUtils.#assertPanelElementVisibility({
+ header: true,
+ betaIcon: true,
+ cancelButton: true,
+ initFailureContent: true,
+ initFailureMessageBar: true,
+ settingsButton: true,
+ tryAgainButton: true,
+ });
+ SharedTranslationsTestUtils._assertTabIndexOrder([
+ settingsButton,
+ ...(AppConstants.platform === "win"
+ ? [tryAgainButton, cancelButton]
+ : [cancelButton, tryAgainButton]),
+ ]);
+ SharedTranslationsTestUtils._assertHasFocus(tryAgainButton);
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel UI matches the expected
+ * state when a translation has failed to complete.
+ */
+ static async assertPanelViewTranslationFailure() {
+ const {
+ cancelButton,
+ fromMenuList,
+ settingsButton,
+ toMenuList,
+ translationFailureMessageBar,
+ tryAgainButton,
+ } = SelectTranslationsPanel.elements;
+ await SelectTranslationsTestUtils.waitForPanelState("translation-failure");
+ SelectTranslationsTestUtils.#assertPanelElementVisibility({
+ header: true,
+ betaIcon: true,
+ cancelButton: true,
+ fromLabel: true,
+ fromMenuList: true,
+ mainContent: true,
+ settingsButton: true,
+ toLabel: true,
+ toMenuList: true,
+ translationFailureMessageBar: true,
+ tryAgainButton: true,
+ });
+ is(
+ document.activeElement,
+ tryAgainButton,
+ "The try-again button should have focus."
+ );
+ is(
+ translationFailureMessageBar.getAttribute("role"),
+ "alert",
+ "The translation failure message bar is an alert."
+ );
+ SharedTranslationsTestUtils._assertTabIndexOrder([
+ settingsButton,
+ fromMenuList,
+ toMenuList,
+ ...(AppConstants.platform === "win"
+ ? [tryAgainButton, cancelButton]
+ : [cancelButton, tryAgainButton]),
+ ]);
+ SharedTranslationsTestUtils._assertHasFocus(tryAgainButton);
+ }
+
+ 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 the SelectTranslationsPanel UI matches the expected
+ * state when the panel has completed its translation.
+ */
+ static async assertPanelViewUnsupportedLanguage() {
+ await SelectTranslationsTestUtils.waitForPanelState("unsupported");
+ const {
+ doneButtonSecondary,
+ settingsButton,
+ translateButton,
+ tryAnotherSourceMenuList,
+ unsupportedLanguageMessageBar,
+ } = SelectTranslationsPanel.elements;
SelectTranslationsTestUtils.#assertPanelElementVisibility({
betaIcon: true,
+ doneButtonSecondary: true,
+ header: true,
+ settingsButton: true,
+ translateButton: true,
+ tryAnotherSourceMenuList: true,
+ unsupportedLanguageContent: true,
+ unsupportedLanguageMessageBar: true,
+ });
+ SelectTranslationsTestUtils.#assertConditionalUIEnabled({
+ doneButtonSecondary: true,
+ translateButton: false,
+ });
+ ok(
+ translateButton.disabled,
+ "The translate button should be disabled when first shown."
+ );
+ SharedTranslationsTestUtils._assertL10nId(
+ unsupportedLanguageMessageBar,
+ "select-translations-panel-unsupported-language-message-known"
+ );
+ SharedTranslationsTestUtils._assertHasFocus(tryAnotherSourceMenuList);
+ SharedTranslationsTestUtils._assertTabIndexOrder([
+ settingsButton,
+ tryAnotherSourceMenuList,
+ doneButtonSecondary,
+ ]);
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel translated text area is
+ * both scrollable and scrolled to the top.
+ */
+ static async #assertPanelTextAreaOverflow() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ if (textArea.style.overflow !== "auto") {
+ await BrowserTestUtils.waitForMutationCondition(
+ textArea,
+ { attributes: true, attributeFilter: ["style"] },
+ () => textArea.style.overflow === "auto"
+ );
+ }
+ if (textArea.scrollHeight > textArea.clientHeight) {
+ info("Ensuring that the textarea is scrolled to the top.");
+ await waitForCondition(() => textArea.scrollTop === 0);
+ }
+ }
+
+ /**
+ * 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 async assertPanelViewActivelyTranslating() {
+ const { textArea } = SelectTranslationsPanel.elements;
+ const isFullPageTranslationsRestrictedForPage =
+ TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser);
+ await SelectTranslationsTestUtils.waitForPanelState("translating");
+ ok(
+ textArea.classList.contains("translating"),
+ "The textarea should have the translating class."
+ );
+ SelectTranslationsTestUtils.#assertPanelElementVisibility({
+ betaIcon: true,
+ copyButton: true,
+ doneButtonPrimary: true,
fromLabel: true,
fromMenuList: true,
header: true,
+ mainContent: true,
+ settingsButton: true,
textArea: true,
toLabel: true,
toMenuList: true,
+ translateFullPageButton: !isFullPageTranslationsRestrictedForPage,
+ });
+ SelectTranslationsTestUtils.#assertPanelHasTranslatingPlaceholder();
+ }
+
+ /**
+ * Asserts that the SelectTranslationsPanel UI contains the
+ * translating placeholder text.
+ */
+ static async #assertPanelHasTranslatingPlaceholder() {
+ const { textArea, fromMenuList, toMenuList } =
+ SelectTranslationsPanel.elements;
+ const expected = await document.l10n.formatValue(
+ "select-translations-panel-translating-placeholder-text"
+ );
+ const isFullPageTranslationsRestrictedForPage =
+ TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser);
+ is(
+ textArea.value,
+ expected,
+ "Active translation text area should have the translating placeholder."
+ );
+ SelectTranslationsTestUtils.#assertPanelTextAreaDirection();
+ SelectTranslationsTestUtils.#assertConditionalUIEnabled({
+ textArea: true,
+ copyButton: false,
+ doneButtonPrimary: true,
+ translateFullPageButton:
+ fromMenuList.value !== toMenuList.value &&
+ !isFullPageTranslationsRestrictedForPage,
+ });
+ }
+
+ /**
+ * 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 isFullPageTranslationsRestrictedForPage =
+ TranslationsParent.isFullPageTranslationsRestrictedForPage(gBrowser);
+
+ SelectTranslationsTestUtils.#assertPanelTextAreaDirection(toLanguage);
+ SelectTranslationsTestUtils.#assertConditionalUIEnabled({
+ textArea: true,
copyButton: true,
- doneButton: true,
- translateFullPageButton: true,
+ doneButtonPrimary: true,
+ translateFullPageButton:
+ fromLanguage !== toLanguage && !isFullPageTranslationsRestrictedForPage,
});
+
+ if (fromLanguage === toLanguage) {
+ is(
+ SelectTranslationsPanel.getSourceText(),
+ SelectTranslationsPanel.getTranslatedText(),
+ "The source text should passthrough as the translated text."
+ );
+ return;
+ }
+
+ 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."
+ );
+ }
+
+ /**
+ * Asserts the enabled state of action buttons in the SelectTranslationsPanel.
+ *
+ * @param {Record<string, boolean>} enabledStates
+ * - An object that maps whether each element should be enabled (true) or disabled (false).
+ */
+ static #assertConditionalUIEnabled(enabledStates) {
+ const elements = SelectTranslationsPanel.elements;
+
+ for (const [elementName, expectEnabled] of Object.entries(enabledStates)) {
+ const element = elements[elementName];
+ if (!element) {
+ throw new Error(
+ `SelectTranslationsPanel element '${elementName}' not found.`
+ );
+ }
+ is(
+ element.disabled,
+ !expectEnabled,
+ `The element '${elementName} should be ${
+ expectEnabled ? "enabled" : "disabled"
+ }.`
+ );
+ }
}
/**
@@ -1464,11 +1991,9 @@ class SelectTranslationsTestUtils {
*
* @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 +2001,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);
+ }
}
/**
@@ -1488,125 +2031,506 @@ class SelectTranslationsTestUtils {
*/
static async clickDoneButton() {
logAction();
- const { doneButton } = SelectTranslationsPanel.elements;
- assertVisibility({ visible: { doneButton } });
+ const { doneButtonPrimary, doneButtonSecondary } =
+ SelectTranslationsPanel.elements;
+ let visibleDoneButton;
+ let hiddenDoneButton;
+ if (BrowserTestUtils.isVisible(doneButtonPrimary)) {
+ visibleDoneButton = doneButtonPrimary;
+ hiddenDoneButton = doneButtonSecondary;
+ } else if (BrowserTestUtils.isVisible(doneButtonSecondary)) {
+ visibleDoneButton = doneButtonSecondary;
+ hiddenDoneButton = doneButtonPrimary;
+ } else {
+ throw new Error(
+ "Expected either the primary or secondary done button to be visible."
+ );
+ }
+ assertVisibility({
+ visible: { visibleDoneButton },
+ hidden: { hiddenDoneButton },
+ });
await SelectTranslationsTestUtils.waitForPanelPopupEvent(
"popuphidden",
() => {
- click(doneButton, "Clicking the done button");
+ click(visibleDoneButton, "Clicking the done button");
}
);
}
/**
+ * Simulates clicking the cancel button and waits for the panel to close.
+ */
+ static async clickCancelButton() {
+ logAction();
+ const { cancelButton } = SelectTranslationsPanel.elements;
+ assertVisibility({ visible: { cancelButton } });
+ await SelectTranslationsTestUtils.waitForPanelPopupEvent(
+ "popuphidden",
+ () => {
+ click(cancelButton, "Clicking the cancel button");
+ }
+ );
+ }
+
+ /**
+ * Simulates clicking the copy button and asserts that all relevant states are correctly updated.
+ */
+ static async clickCopyButton() {
+ logAction();
+ const { copyButton } = SelectTranslationsPanel.elements;
+
+ assertVisibility({ visible: { copyButton } });
+ is(
+ SelectTranslationsPanel.phase(),
+ "translated",
+ 'The copy button should only be clickable in the "translated" phase'
+ );
+
+ click(copyButton, "Clicking the copy button");
+ await waitForCondition(
+ () =>
+ copyButton.classList.contains("copied") &&
+ copyButton.getAttribute("data-l10n-id") ===
+ "select-translations-panel-copy-button-copied",
+ "Waiting for copy button to match the copied state."
+ );
+
+ const copiedText = SpecialPowers.getClipboardData("text/plain");
+ is(
+ // Because of differences in the clipboard code on Windows, we are going
+ // to explicitly sanitize carriage returns here when checking equality.
+ copiedText.replaceAll("\r", ""),
+ SelectTranslationsPanel.getTranslatedText().replaceAll("\r", ""),
+ "The clipboard should contain the translated text."
+ );
+ }
+
+ /**
+ * Simulates clicking the Translate button in the SelectTranslationsPanel,
+ * then waits for any pending translation effects, based on the provided options.
+ *
+ * @param {object} config
+ * @param {Function} [config.downloadHandler]
+ * - The function handle expected downloads, resolveDownloads() or rejectDownloads()
+ * Leave as null to test more granularly, such as testing opening the loading view,
+ * or allowing for the automatic downloading of files.
+ * @param {boolean} [config.pivotTranslation]
+ * - True if the expected translation is a pivot translation, otherwise false.
+ * Affects the number of expected downloads.
+ * @param {Function} [config.viewAssertion]
+ * - An optional callback function to execute for asserting the panel UI state.
+ */
+ static async clickTranslateButton({
+ downloadHandler,
+ pivotTranslation,
+ viewAssertion,
+ }) {
+ logAction();
+ const {
+ doneButtonSecondary,
+ settingsButton,
+ translateButton,
+ tryAnotherSourceMenuList,
+ } = SelectTranslationsPanel.elements;
+ assertVisibility({ visible: { doneButtonPrimary: translateButton } });
+
+ ok(!translateButton.disabled, "The translate button should be enabled.");
+ SharedTranslationsTestUtils._assertTabIndexOrder([
+ settingsButton,
+ tryAnotherSourceMenuList,
+ ...(AppConstants.platform === "win"
+ ? [translateButton, doneButtonSecondary]
+ : [doneButtonSecondary, translateButton]),
+ ]);
+
+ click(translateButton);
+ await SelectTranslationsTestUtils.waitForPanelState("translatable");
+ if (downloadHandler) {
+ await this.handleDownloads({ downloadHandler, pivotTranslation });
+ }
+ if (viewAssertion) {
+ await viewAssertion();
+ }
+ }
+
+ /**
+ * Simulates clicking the translate-full-page button.
+ */
+ static async clickTranslateFullPageButton() {
+ logAction();
+ const { translateFullPageButton } = SelectTranslationsPanel.elements;
+ assertVisibility({ visible: { translateFullPageButton } });
+ click(translateFullPageButton);
+ await FullPageTranslationsTestUtils.assertTranslationsButton(
+ { button: true, circleArrows: true, locale: false, icon: true },
+ "The icon presents the loading indicator."
+ );
+ }
+
+ /**
+ * Simulates clicking the try-again button.
+ *
+ * @param {object} config
+ * @param {Function} [config.downloadHandler]
+ * - The function handle expected downloads, resolveDownloads() or rejectDownloads()
+ * Leave as null to test more granularly, such as testing opening the loading view,
+ * or allowing for the automatic downloading of files.
+ * @param {boolean} [config.pivotTranslation]
+ * - True if the expected translation is a pivot translation, otherwise false.
+ * Affects the number of expected downloads.
+ * @param {Function} [config.viewAssertion]
+ * - An optional callback function to execute for asserting the panel UI state.
+ */
+ static async clickTryAgainButton({
+ downloadHandler,
+ pivotTranslation,
+ viewAssertion,
+ } = {}) {
+ logAction();
+ const { tryAgainButton } = SelectTranslationsPanel.elements;
+ assertVisibility({ visible: { tryAgainButton } });
+ click(tryAgainButton, "Clicking the try-again button");
+ await SelectTranslationsTestUtils.waitForPanelState("translatable");
+ if (downloadHandler) {
+ await this.handleDownloads({ downloadHandler, pivotTranslation });
+ }
+ if (viewAssertion) {
+ await viewAssertion();
+ }
+ }
+
+ /**
+ * Opens the SelectTranslationsPanel settings menu.
+ * Requires that the translations panel is already open.
+ */
+ static async openPanelSettingsMenu() {
+ logAction();
+ const { settingsButton } = SelectTranslationsPanel.elements;
+ assertVisibility({ visible: { settingsButton } });
+ await SharedTranslationsTestUtils._waitForPopupEvent(
+ "select-translations-panel-settings-menupopup",
+ "popupshown",
+ () => click(settingsButton, "Opening the settings menu")
+ );
+ const settingsPageMenuItem = document.getElementById(
+ "select-translations-panel-open-settings-page-menuitem"
+ );
+ const aboutTranslationsMenuItem = document.getElementById(
+ "select-translations-panel-about-translations-menuitem"
+ );
+
+ assertVisibility({
+ visible: {
+ settingsPageMenuItem,
+ aboutTranslationsMenuItem,
+ },
+ });
+ }
+
+ /**
+ * Clicks the SelectTranslationsPanel settings menu item
+ * that leads to the Translations Settings in about:preferences.
+ */
+ static clickTranslationsSettingsPageMenuItem() {
+ logAction();
+ const settingsPageMenuItem = document.getElementById(
+ "select-translations-panel-open-settings-page-menuitem"
+ );
+ assertVisibility({ visible: { settingsPageMenuItem } });
+ click(settingsPageMenuItem);
+ }
+
+ /**
* Opens the context menu at a specified element on the page, based on the provided options.
*
* @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.
+ *
+ * @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.
+ *
+ * The following options will work on all test pages that have an <h1> element.
+ *
+ * @param {boolean} options.selectH1 - Selects the first H1 element of the page.
+ * @param {boolean} options.openAtH1 - Opens the context menu at the first H1 element of the page.
+ *
+ * The following options will work only in the PDF_TEST_PAGE_URL.
+ *
+ * @param {boolean} options.selectPdfSpan - Selects the first span of text on the first page of a pdf.
+ * @param {boolean} options.openAtPdfSpan - Opens the context menu at the first span of text on the first page of a pdf.
+ *
+ * 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 element = await selectorFunction();
+ TranslationsTest.selectContentElement(element);
+ }
+ },
+ { selectorFunctionName }
+ );
+ }
+ };
- if (selectSpanishParagraph === true) {
- await runInPage(async TranslationsTest => {
- const { getSpanishParagraph } = TranslationsTest.getSelectors();
- const paragraph = getSpanishParagraph();
- TranslationsTest.selectContentElement(paragraph);
- });
+ await maybeSelectContentFrom("H1");
+ await maybeSelectContentFrom("PdfSpan");
+ 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 = await selectorFunction();
+ await TranslationsTest.rightClickContentElement(element);
+ }
+ },
+ { selectorFunctionName }
+ );
+ }
+ );
+ }
+ };
+
+ await maybeOpenContextMenuAt("H1");
+ await maybeOpenContextMenuAt("PdfSpan");
+ 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 }) {
+ if (downloadHandler) {
+ 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);
- });
- }
+ /**
+ * 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) {
+ logAction(langTags);
+ const { fromMenuList, fromMenuPopup } = SelectTranslationsPanel.elements;
+ const { openDropdownMenu } = options;
+
+ const switchFn = openDropdownMenu
+ ? SelectTranslationsTestUtils.#changeSelectedLanguageViaDropdownMenu
+ : SelectTranslationsTestUtils.#changeSelectedLanguageDirectly;
+
+ await switchFn(
+ langTags,
+ { menuList: fromMenuList, menuPopup: fromMenuPopup },
+ options
+ );
+ }
+
+ /**
+ * Change the selected language in the try-another-source-language dropdown.
+ *
+ * @param {string} langTag - A BCP-47 language tag.
+ */
+ static async changeSelectedTryAnotherSourceLanguage(langTag) {
+ logAction(langTag);
+ const { tryAnotherSourceMenuList, translateButton } =
+ SelectTranslationsPanel.elements;
+ await SelectTranslationsTestUtils.#changeSelectedLanguageDirectly(
+ [langTag],
+ { menuList: tryAnotherSourceMenuList },
+ {
+ onChangeLanguage: () =>
+ ok(
+ !translateButton.disabled,
+ "The translate button should be enabled after selecting a language."
+ ),
+ }
+ );
+ }
+
+ /**
+ * 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) {
+ logAction(langTags);
+ 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 { textArea } = SelectTranslationsPanel.elements;
+ const { onChangeLanguage, downloadHandler } = options;
+
+ for (const langTag of langTags) {
+ const menuListUpdated = BrowserTestUtils.waitForMutationCondition(
+ menuList,
+ { attributes: true, attributeFilter: ["value"] },
+ () => menuList.value === langTag
);
- return;
+
+ menuList.focus();
+ menuList.value = langTag;
+ menuList.dispatchEvent(new Event("command"));
+ await menuListUpdated;
}
- if (openAtSpanishParagraph === true) {
- await SharedTranslationsTestUtils._waitForPopupEvent(
- "contentAreaContextMenu",
- "popupshown",
- async () => {
- await runInPage(async TranslationsTest => {
- const { getSpanishParagraph } = TranslationsTest.getSelectors();
- const paragraph = getSpanishParagraph();
- await TranslationsTest.rightClickContentElement(paragraph);
- });
- }
- );
- return;
+ // Either of these events should trigger a translation after the selected
+ // language has been changed directly.
+ if (Math.random() < 0.5) {
+ info("Attempting to trigger translation via text-area focus.");
+ textArea.focus();
+ } else {
+ info("Attempting to trigger translation via pressing Enter.");
+ EventUtils.synthesizeKey("KEY_Enter");
}
- 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) {
+ 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 +2538,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 +2572,7 @@ class SelectTranslationsTestUtils {
await SelectTranslationsTestUtils.assertContextMenuTranslateSelectionItem(
runInPage,
- {
- selectFirstParagraph,
- selectSpanishParagraph,
- expectedTargetLanguage,
- openAtFirstParagraph,
- openAtSpanishParagraph,
- openAtEnglishHyperlink,
- openAtSpanishHyperlink,
- },
+ options,
message
);
@@ -1668,9 +2580,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 +2663,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 +2675,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_UITour5.js b/browser/components/uitour/test/browser_UITour5.js
index 50316d4225..ebb0811440 100644
--- a/browser/components/uitour/test/browser_UITour5.js
+++ b/browser/components/uitour/test/browser_UITour5.js
@@ -41,7 +41,7 @@ add_UITour_task(async function test_highlight_help_and_show_help_subview() {
let helpButtonID = "appMenu-help-button2";
let helpBtn = document.getElementById(helpButtonID);
- helpBtn.dispatchEvent(new Event("command"));
+ helpBtn.dispatchEvent(new Event("command", { bubbles: true }));
await highlightHiddenPromise;
await ViewShownPromise;
let helpView = document.getElementById("PanelUI-helpView");
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/ActionsProvider.sys.mjs b/browser/components/urlbar/ActionsProvider.sys.mjs
new file mode 100644
index 0000000000..9cd99969a2
--- /dev/null
+++ b/browser/components/urlbar/ActionsProvider.sys.mjs
@@ -0,0 +1,110 @@
+/* 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/. */
+
+/**
+ * A provider that matches the urlbar input to built in actions.
+ */
+export class ActionsProvider {
+ /**
+ * Unique name for the provider.
+ *
+ * @abstract
+ */
+ get name() {
+ return "ActionsProviderBase";
+ }
+
+ /**
+ * Whether this provider should be invoked for the given context.
+ * If this method returns false, the providers manager won't start a query
+ * with this provider, to save on resources.
+ *
+ * @param {UrlbarQueryContext} _queryContext The query context object.
+ * @returns {boolean} Whether this provider should be invoked for the search.
+ * @abstract
+ */
+ isActive(_queryContext) {
+ throw new Error("Not implemented.");
+ }
+
+ /**
+ * Query for actions based on the current users input.
+ *
+ * @param {UrlbarQueryContext} _queryContext The query context object.
+ * @param {UrlbarController} _controller The urlbar controller.
+ * @returns {ActionsResult}
+ * @abstract
+ */
+ async queryAction(_queryContext, _controller) {
+ throw new Error("Not implemented.");
+ }
+
+ /**
+ * Pick an action.
+ *
+ * @param {UrlbarQueryContext} _queryContext The query context object.
+ * @param {UrlbarController} _controller The urlbar controller.
+ * @param {DOMElement} _element The element that was selected.
+ * @abstract
+ */
+ pickAction(_queryContext, _controller, _element) {
+ throw new Error("Not implemented.");
+ }
+}
+
+/**
+ * Class used to create an Actions Result.
+ */
+export class ActionsResult {
+ providerName;
+
+ #key;
+ #l10nId;
+ #l10nArgs;
+ #icon;
+ #dataset;
+
+ /**
+ * @param {object} options
+ * An option object.
+ * @param { string } options.key
+ * A string key used to distinguish between different actions.
+ * @param { string } options.l10nId
+ * The id of the l10n string displayed in the action button.
+ * @param { string } options.l10nArgs
+ * Arguments passed to construct the above string
+ * @param { string } options.icon
+ * The icon displayed in the button.
+ * @param {object} options.dataset
+ * An object of properties we set on the action button that
+ * can be used to pass data when it is selected.
+ */
+ constructor({ key, l10nId, l10nArgs, icon, dataset }) {
+ this.#key = key;
+ this.#l10nId = l10nId;
+ this.#l10nArgs = l10nArgs;
+ this.#icon = icon;
+ this.#dataset = dataset;
+ }
+
+ get key() {
+ return this.#key;
+ }
+
+ get l10nId() {
+ return this.#l10nId;
+ }
+
+ get l10nArgs() {
+ return this.#l10nArgs;
+ }
+
+ get icon() {
+ return this.#icon;
+ }
+
+ get dataset() {
+ return this.#dataset;
+ }
+}
diff --git a/browser/components/urlbar/ActionsProviderContextualSearch.sys.mjs b/browser/components/urlbar/ActionsProviderContextualSearch.sys.mjs
new file mode 100644
index 0000000000..58af7c94c5
--- /dev/null
+++ b/browser/components/urlbar/ActionsProviderContextualSearch.sys.mjs
@@ -0,0 +1,121 @@
+/* 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 { UrlbarUtils } from "resource:///modules/UrlbarUtils.sys.mjs";
+
+import {
+ ActionsProvider,
+ ActionsResult,
+} from "resource:///modules/ActionsProvider.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ OpenSearchEngine: "resource://gre/modules/OpenSearchEngine.sys.mjs",
+ loadAndParseOpenSearchEngine:
+ "resource://gre/modules/OpenSearchLoader.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
+});
+
+const ENABLED_PREF = "contextualSearch.enabled";
+
+/**
+ * A provider that returns an option for using the search engine provided
+ * by the active view if it utilizes OpenSearch.
+ */
+class ProviderContextualSearch extends ActionsProvider {
+ constructor() {
+ super();
+ this.engines = new Map();
+ }
+
+ get name() {
+ return "ActionsProviderContextualSearch";
+ }
+
+ isActive(queryContext) {
+ return (
+ queryContext.trimmedSearchString &&
+ lazy.UrlbarPrefs.get(ENABLED_PREF) &&
+ !queryContext.searchMode
+ );
+ }
+
+ async queryAction(queryContext, controller) {
+ let instance = this.queryInstance;
+ const hostname = URL.parse(queryContext.currentPage)?.hostname;
+
+ // This happens on about pages, which won't have associated engines
+ if (!hostname) {
+ return null;
+ }
+
+ let engine = await this.fetchEngine(controller);
+ let icon = engine?.icon || (await engine?.getIconURL?.());
+ let defaultEngine = lazy.UrlbarSearchUtils.getDefaultEngine();
+
+ if (
+ !engine ||
+ engine.name === defaultEngine?.name ||
+ instance != this.queryInstance
+ ) {
+ return null;
+ }
+
+ return new ActionsResult({
+ key: "contextual-search",
+ l10nId: "urlbar-result-search-with",
+ l10nArgs: { engine: engine.name || engine.title },
+ icon,
+ });
+ }
+
+ async fetchEngine(controller) {
+ let browser = controller.browserWindow.gBrowser.selectedBrowser;
+ let hostname = browser?.currentURI.host;
+
+ if (this.engines.has(hostname)) {
+ return this.engines.get(hostname);
+ }
+
+ // Strip www. to allow for partial matches when looking for an engine.
+ const [host] = UrlbarUtils.stripPrefixAndTrim(hostname, {
+ stripWww: true,
+ });
+ let engines = await lazy.UrlbarSearchUtils.enginesForDomainPrefix(host, {
+ matchAllDomainLevels: true,
+ });
+ return engines[0] ?? browser?.engines?.[0];
+ }
+
+ async pickAction(queryContext, controller, element) {
+ // If we have an engine to add, first create a new OpenSearchEngine, then
+ // get and open a url to execute a search for the term in the url bar.
+ let engine = await this.fetchEngine(controller);
+
+ if (engine.uri) {
+ let engineData = await lazy.loadAndParseOpenSearchEngine(
+ Services.io.newURI(engine.uri)
+ );
+ engine = new lazy.OpenSearchEngine({ engineData });
+ engine._setIcon(engine.icon, false);
+ }
+
+ const [url] = UrlbarUtils.getSearchQueryUrl(
+ engine,
+ queryContext.searchString
+ );
+ element.ownerGlobal.gBrowser.fixupAndLoadURIString(url, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ element.ownerGlobal.gBrowser.selectedBrowser.focus();
+ }
+
+ resetForTesting() {
+ this.engines = new Map();
+ }
+}
+
+export var ActionsProviderContextualSearch = new ProviderContextualSearch();
diff --git a/browser/components/urlbar/ActionsProviderQuickActions.sys.mjs b/browser/components/urlbar/ActionsProviderQuickActions.sys.mjs
new file mode 100644
index 0000000000..a693e7686a
--- /dev/null
+++ b/browser/components/urlbar/ActionsProviderQuickActions.sys.mjs
@@ -0,0 +1,152 @@
+/* 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 {
+ ActionsProvider,
+ ActionsResult,
+} from "resource:///modules/ActionsProvider.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ QuickActionsLoaderDefault:
+ "resource:///modules/QuickActionsLoaderDefault.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+});
+
+// These prefs are relative to the `browser.urlbar` branch.
+const ENABLED_PREF = "quickactions.enabled";
+const MATCH_IN_PHRASE_PREF = "quickactions.matchInPhrase";
+const MIN_SEARCH_PREF = "quickactions.minimumSearchString";
+
+/**
+ * A provider that matches the urlbar input to built in actions.
+ */
+class ProviderQuickActions extends ActionsProvider {
+ get name() {
+ return "ActionsProviderQuickActions";
+ }
+
+ isActive(queryContext) {
+ return (
+ lazy.UrlbarPrefs.get(ENABLED_PREF) &&
+ !queryContext.searchMode &&
+ queryContext.trimmedSearchString.length < 50 &&
+ queryContext.trimmedSearchString.length >
+ lazy.UrlbarPrefs.get(MIN_SEARCH_PREF)
+ );
+ }
+
+ async queryAction(queryContext) {
+ await lazy.QuickActionsLoaderDefault.ensureLoaded();
+ let input = queryContext.trimmedLowerCaseSearchString;
+ let results = [...(this.#prefixes.get(input) ?? [])];
+
+ if (lazy.UrlbarPrefs.get(MATCH_IN_PHRASE_PREF)) {
+ for (let [keyword, key] of this.#keywords) {
+ if (input.includes(keyword)) {
+ results.push(key);
+ }
+ }
+ }
+
+ // Remove invisible actions.
+ results = results.filter(key => {
+ const action = this.#actions.get(key);
+ return action.isVisible?.() ?? true;
+ });
+
+ if (!results.length) {
+ return null;
+ }
+
+ let action = this.#actions.get(results[0]);
+ return new ActionsResult({
+ key: results[0],
+ l10nId: action.label,
+ icon: action.icon,
+ dataset: {
+ action: results[0],
+ inputLength: queryContext.trimmedSearchString.length,
+ },
+ });
+ }
+
+ pickAction(_queryContext, _controller, element) {
+ let action = element.dataset.action;
+ let inputLength = Math.min(element.dataset.inputLength, 10);
+ Services.telemetry.keyedScalarAdd(
+ `quickaction.picked`,
+ `${action}-${inputLength}`,
+ 1
+ );
+ let options = this.#actions.get(action).onPick();
+ if (options?.focusContent) {
+ element.ownerGlobal.gBrowser.selectedBrowser.focus();
+ }
+ }
+
+ /**
+ * Adds a new QuickAction.
+ *
+ * @param {string} key A key to identify this action.
+ * @param {string} definition An object that describes the action.
+ */
+ addAction(key, definition) {
+ this.#actions.set(key, definition);
+ definition.commands.forEach(cmd => this.#keywords.set(cmd, key));
+ this.#loopOverPrefixes(definition.commands, prefix => {
+ let result = this.#prefixes.get(prefix);
+ if (result) {
+ if (!result.includes(key)) {
+ result.push(key);
+ }
+ } else {
+ result = [key];
+ }
+ this.#prefixes.set(prefix, result);
+ });
+ }
+
+ /**
+ * Removes an action.
+ *
+ * @param {string} key A key to identify this action.
+ */
+ removeAction(key) {
+ let definition = this.#actions.get(key);
+ this.#actions.delete(key);
+ definition.commands.forEach(cmd => this.#keywords.delete(cmd));
+ this.#loopOverPrefixes(definition.commands, prefix => {
+ let result = this.#prefixes.get(prefix);
+ if (result) {
+ result = result.filter(val => val != key);
+ }
+ this.#prefixes.set(prefix, result);
+ });
+ }
+
+ // A map from keywords to an action.
+ #keywords = new Map();
+
+ // A map of all prefixes to an array of actions.
+ #prefixes = new Map();
+
+ // The actions that have been added.
+ #actions = new Map();
+
+ #loopOverPrefixes(commands, fun) {
+ for (const command of commands) {
+ // Loop over all the prefixes of the word, ie
+ // "", "w", "wo", "wor", stopping just before the full
+ // word itself which will be matched by the whole
+ // phrase matching.
+ for (let i = 1; i <= command.length; i++) {
+ let prefix = command.substring(0, command.length - i);
+ fun(prefix);
+ }
+ }
+ }
+}
+
+export var ActionsProviderQuickActions = new ProviderQuickActions();
diff --git a/browser/components/urlbar/QuickActionsLoaderDefault.sys.mjs b/browser/components/urlbar/QuickActionsLoaderDefault.sys.mjs
index 0ab9c4c83e..9a7b7a6a34 100644
--- a/browser/components/urlbar/QuickActionsLoaderDefault.sys.mjs
+++ b/browser/components/urlbar/QuickActionsLoaderDefault.sys.mjs
@@ -8,12 +8,10 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
- ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs",
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs",
- UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
- UrlbarProviderQuickActions:
- "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+ ActionsProviderQuickActions:
+ "resource:///modules/ActionsProviderQuickActions.sys.mjs",
});
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
@@ -26,7 +24,6 @@ if (AppConstants.MOZ_UPDATER) {
"nsIApplicationUpdateService"
);
}
-
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"SCREENSHOT_BROWSER_COMPONENT",
@@ -54,7 +51,7 @@ let openUrl = url => {
let openAddonsUrl = url => {
return () => {
let window = lazy.BrowserWindowTracker.getTopWindow();
- window.BrowserOpenAddonsMgr(url, { selectTabByViewId: true });
+ window.BrowserAddonUI.openAddonsMgr(url, { selectTabByViewId: true });
};
};
@@ -97,24 +94,22 @@ const DEFAULT_ACTIONS = {
},
},
downloads: {
- l10nCommands: ["quickactions-cmd-downloads", "quickactions-downloads2"],
+ l10nCommands: ["quickactions-cmd-downloads"],
icon: "chrome://browser/skin/downloads/downloads.svg",
label: "quickactions-downloads2",
onPick: openUrlFun("about:downloads"),
},
extensions: {
- l10nCommands: ["quickactions-cmd-extensions", "quickactions-extensions"],
+ l10nCommands: ["quickactions-cmd-extensions"],
icon: "chrome://mozapps/skin/extensions/category-extensions.svg",
label: "quickactions-extensions",
onPick: openAddonsUrl("addons://list/extension"),
},
inspect: {
- l10nCommands: ["quickactions-cmd-inspector", "quickactions-inspector2"],
+ l10nCommands: ["quickactions-cmd-inspector"],
icon: "chrome://devtools/skin/images/open-inspector.svg",
label: "quickactions-inspector2",
- isVisible: () =>
- lazy.DevToolsShim.isEnabled() || lazy.DevToolsShim.isDevToolsUser(),
- isActive: () => {
+ isVisible: () => {
// The inspect action is available if:
// 1. DevTools is enabled.
// 2. The user can be considered as a DevTools user.
@@ -132,18 +127,18 @@ const DEFAULT_ACTIONS = {
onPick: openInspector,
},
logins: {
- l10nCommands: ["quickactions-cmd-logins", "quickactions-logins2"],
+ l10nCommands: ["quickactions-cmd-logins"],
label: "quickactions-logins2",
onPick: openUrlFun("about:logins"),
},
plugins: {
- l10nCommands: ["quickactions-cmd-plugins", "quickactions-plugins"],
+ l10nCommands: ["quickactions-cmd-plugins"],
icon: "chrome://mozapps/skin/extensions/category-extensions.svg",
label: "quickactions-plugins",
onPick: openAddonsUrl("addons://list/plugin"),
},
print: {
- l10nCommands: ["quickactions-cmd-print", "quickactions-print2"],
+ l10nCommands: ["quickactions-cmd-print"],
label: "quickactions-print2",
icon: "chrome://global/skin/icons/print.svg",
onPick: () => {
@@ -153,7 +148,7 @@ const DEFAULT_ACTIONS = {
},
},
private: {
- l10nCommands: ["quickactions-cmd-private", "quickactions-private2"],
+ l10nCommands: ["quickactions-cmd-private"],
label: "quickactions-private2",
icon: "chrome://global/skin/icons/indicator-private-browsing.svg",
onPick: () => {
@@ -163,7 +158,7 @@ const DEFAULT_ACTIONS = {
},
},
refresh: {
- l10nCommands: ["quickactions-cmd-refresh", "quickactions-refresh"],
+ l10nCommands: ["quickactions-cmd-refresh"],
label: "quickactions-refresh",
onPick: () => {
lazy.ResetProfile.openConfirmationDialog(
@@ -172,7 +167,7 @@ const DEFAULT_ACTIONS = {
},
},
restart: {
- l10nCommands: ["quickactions-cmd-restart", "quickactions-restart"],
+ l10nCommands: ["quickactions-cmd-restart"],
label: "quickactions-restart",
onPick: restartBrowser,
},
@@ -197,10 +192,10 @@ const DEFAULT_ACTIONS = {
},
},
screenshot: {
- l10nCommands: ["quickactions-cmd-screenshot", "quickactions-screenshot3"],
+ l10nCommands: ["quickactions-cmd-screenshot"],
label: "quickactions-screenshot3",
icon: "chrome://browser/skin/screenshot.svg",
- isActive: () => {
+ isVisible: () => {
return !lazy.BrowserWindowTracker.getTopWindow().gScreenshots.shouldScreenshotsButtonBeDisabled();
},
onPick: () => {
@@ -221,21 +216,21 @@ const DEFAULT_ACTIONS = {
},
},
settings: {
- l10nCommands: ["quickactions-cmd-settings", "quickactions-settings2"],
+ l10nCommands: ["quickactions-cmd-settings"],
icon: "chrome://global/skin/icons/settings.svg",
label: "quickactions-settings2",
onPick: openUrlFun("about:preferences"),
},
themes: {
- l10nCommands: ["quickactions-cmd-themes", "quickactions-themes"],
+ l10nCommands: ["quickactions-cmd-themes"],
icon: "chrome://mozapps/skin/extensions/category-extensions.svg",
label: "quickactions-themes",
onPick: openAddonsUrl("addons://list/theme"),
},
update: {
- l10nCommands: ["quickactions-cmd-update", "quickactions-update"],
+ l10nCommands: ["quickactions-cmd-update"],
label: "quickactions-update",
- isActive: () => {
+ isVisible: () => {
if (!AppConstants.MOZ_UPDATER) {
return false;
}
@@ -246,10 +241,10 @@ const DEFAULT_ACTIONS = {
onPick: restartBrowser,
},
viewsource: {
- l10nCommands: ["quickactions-cmd-viewsource", "quickactions-viewsource2"],
+ l10nCommands: ["quickactions-cmd-viewsource"],
icon: "chrome://global/skin/icons/settings.svg",
label: "quickactions-viewsource2",
- isActive: () => currentBrowser()?.currentURI.scheme !== "view-source",
+ isVisible: () => currentBrowser()?.currentURI.scheme !== "view-source",
onPick: () => openUrl("view-source:" + currentBrowser().currentURI.spec),
},
};
@@ -287,18 +282,6 @@ function restartBrowser() {
}
}
-function random(seed) {
- let x = Math.sin(seed) * 10000;
- return x - Math.floor(x);
-}
-
-function shuffle(array, seed) {
- for (let i = array.length - 1; i > 0; i--) {
- const j = Math.floor(random(seed) * (i + 1));
- [array[i], array[j]] = [array[j], array[i]];
- }
-}
-
/**
* Loads the default QuickActions.
*/
@@ -308,18 +291,6 @@ export class QuickActionsLoaderDefault {
static async load() {
let keys = Object.keys(DEFAULT_ACTIONS);
- if (lazy.UrlbarPrefs.get("quickactions.randomOrderActions")) {
- // We insert the actions in a random order which means they will be returned
- // in a random but consistent order (the order of results for "view" and "views"
- // should be the same).
- // We use the Nimbus randomizationId as the seed as the order should not change
- // for the user between restarts, it should be random between users but a user should
- // see actions the same order.
- let seed = [...lazy.ClientEnvironment.randomizationId]
- .map(x => x.charCodeAt(0))
- .reduce((sum, a) => sum + a, 0);
- shuffle(keys, seed);
- }
for (const key of keys) {
let actionData = DEFAULT_ACTIONS[key];
let messages = await lazy.gFluentStrings.formatMessages(
@@ -328,7 +299,7 @@ export class QuickActionsLoaderDefault {
actionData.commands = messages
.map(({ value }) => value.split(",").map(x => x.trim().toLowerCase()))
.flat();
- lazy.UrlbarProviderQuickActions.addAction(key, actionData);
+ lazy.ActionsProviderQuickActions.addAction(key, actionData);
}
}
static async ensureLoaded() {
diff --git a/browser/components/urlbar/UrlbarController.sys.mjs b/browser/components/urlbar/UrlbarController.sys.mjs
index 9bfc3a645d..5172c14943 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,
@@ -987,7 +1001,6 @@ class TelemetryEvent {
searchWords,
searchSource,
searchMode,
- selectedElement,
selIndex,
selType,
}
@@ -1031,11 +1044,7 @@ class TelemetryEvent {
currentResults[selIndex],
selType
);
- const selected_result_subtype =
- lazy.UrlbarUtils.searchEngagementTelemetrySubtype(
- currentResults[selIndex],
- selectedElement
- );
+ const selected_result_subtype = "";
if (selected_result === "input_field" && !this._controller.view?.isOpen) {
numResults = 0;
@@ -1091,23 +1100,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 +1107,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 +1188,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 +1384,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..2ee463dda8 100644
--- a/browser/components/urlbar/UrlbarInput.sys.mjs
+++ b/browser/components/urlbar/UrlbarInput.sys.mjs
@@ -11,6 +11,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs",
BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
ExtensionSearchHandler:
"resource://gre/modules/ExtensionSearchHandler.sys.mjs",
ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
@@ -24,6 +25,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarQueryContext: "resource:///modules/UrlbarUtils.sys.mjs",
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
@@ -330,8 +332,6 @@ export class UrlbarInput {
}
setSelectionRange(selectionStart, selectionEnd) {
- this.focus();
-
let beforeSelect = new CustomEvent("beforeselect", {
bubbles: true,
cancelable: true,
@@ -348,6 +348,32 @@ export class UrlbarInput {
this._suppressPrimaryAdjustment = false;
}
+ saveSelectionStateForBrowser(browser) {
+ let state = this.#browserStates.get(browser);
+ if (!state) {
+ state = {};
+ this.#browserStates.set(browser, state);
+ }
+ state.selection = {
+ start: this.selectionStart,
+ end: this.selectionEnd,
+ // When restoring a URI from an empty value, we don't want to untrim it.
+ shouldUntrim: this.value && !this._protocolIsTrimmed,
+ };
+ }
+
+ restoreSelectionStateForBrowser(browser) {
+ // Address bar must be focused to untrim and for selection to make sense.
+ this.focus();
+ let state = this.#browserStates.get(browser);
+ if (state?.selection) {
+ if (state.selection.shouldUntrim) {
+ this.#maybeUntrimUrl();
+ }
+ this.setSelectionRange(state.selection.start, state.selection.end);
+ }
+ }
+
/**
* Sets the URI to display in the location bar.
*
@@ -900,6 +926,15 @@ export class UrlbarInput {
if (!result) {
return;
}
+ if (element?.dataset.action && element?.dataset.action != "tabswitch") {
+ this.view.close();
+ let provider = lazy.UrlbarProvidersManager.getActionProvider(
+ element.dataset.providerName
+ );
+ let { queryContext } = this.controller._lastQueryContextWrapper || {};
+ provider.pickAction(queryContext, this.controller, element);
+ return;
+ }
this.pickResult(result, event, element);
}
@@ -1053,7 +1088,13 @@ export class UrlbarInput {
break;
}
case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
- if (this.hasAttribute("action-override")) {
+ // Behaviour is reversed with SecondaryActions, default behaviour is to navigate
+ // and button is provided to switch to tab.
+ if (
+ this.hasAttribute("action-override") ||
+ (lazy.UrlbarPrefs.get("secondaryActions.featureGate") &&
+ element?.dataset.action !== "tabswitch")
+ ) {
where = "current";
break;
}
@@ -1364,7 +1405,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 +1596,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 +1981,7 @@ export class UrlbarInput {
}
set value(val) {
- this._setValue(val, true);
+ this._setValue(val, { allowTrim: true });
}
get lastSearchString() {
@@ -2107,7 +2148,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 +2294,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,19 +2303,38 @@ 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.
+ * @param {string} [options.untrimmedValue] Override for this._untrimmedValue.
+ * @param {boolean} [options.valueIsTyped] Override for this.valueIsTypede.
+ *
+ *
+ * @returns {string} The set value.
+ */
+ _setValue(
+ val,
+ { allowTrim = false, untrimmedValue = null, valueIsTyped = false } = {}
+ ) {
// Don't expose internal about:reader URLs to the user.
let originalUrl = lazy.ReaderMode.getOriginalUrlObjectForDisplay(val);
if (originalUrl) {
val = originalUrl.displaySpec;
}
- this._untrimmedValue = val;
-
+ this._untrimmedValue = untrimmedValue ?? val;
+ this._protocolIsTrimmed = false;
if (allowTrim) {
+ let oldVal = val;
val = this._trimValue(val);
+ this._protocolIsTrimmed =
+ oldVal.startsWith(lazy.BrowserUIUtils.trimURLProtocol) &&
+ !val.startsWith(lazy.BrowserUIUtils.trimURLProtocol);
}
- this.valueIsTyped = false;
+ this.valueIsTyped = valueIsTyped;
this._resultForCurrentValue = null;
this.inputField.value = val;
this.formatValue();
@@ -2395,6 +2451,7 @@ export class UrlbarInput {
selectionEnd: autofillValue.length,
type: this._autofillPlaceholder.type,
adaptiveHistoryInput: this._autofillPlaceholder.adaptiveHistoryInput,
+ untrimmedValue: this._autofillPlaceholder.untrimmedValue,
});
}
@@ -2720,6 +2777,8 @@ export class UrlbarInput {
* @param {string} options.adaptiveHistoryInput
* If the autofill type is "adaptive", this is the matching `input` value
* from adaptive history.
+ * @param {string} options.untrimmedValue
+ * Untrimmed value including a protocol.
*/
_autofillValue({
value,
@@ -2727,10 +2786,11 @@ export class UrlbarInput {
selectionEnd,
type,
adaptiveHistoryInput,
+ untrimmedValue,
}) {
// 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, { untrimmedValue });
this.inputField.setSelectionRange(selectionStart, selectionEnd);
this._autofillPlaceholder = {
value,
@@ -2738,6 +2798,7 @@ export class UrlbarInput {
adaptiveHistoryInput,
selectionStart,
selectionEnd,
+ untrimmedValue,
};
}
@@ -2988,7 +3049,7 @@ export class UrlbarInput {
// pressed, open in current tab to allow ctrl-enter to canonize URL.
where = "current";
} else {
- where = this.window.whereToOpenLink(event, false, false);
+ where = lazy.BrowserUtils.whereToOpenLink(event, false, false);
}
if (lazy.UrlbarPrefs.get("openintab")) {
if (where == "current") {
@@ -3054,7 +3115,12 @@ export class UrlbarInput {
// Error check occurs during isClipboardURIValid
uri = Services.io.newURI(copyString);
- strippedURI = lazy.QueryStringStripper.stripForCopyOrShare(uri);
+ try {
+ strippedURI = lazy.QueryStringStripper.stripForCopyOrShare(uri);
+ } catch (e) {
+ console.warn(`stripForCopyOrShare: ${e.message}`);
+ return uri;
+ }
if (strippedURI) {
return this.makeURIReadable(strippedURI);
@@ -3082,6 +3148,59 @@ export class UrlbarInput {
return true;
}
+ /**
+ * Restores the untrimmed value in the urlbar.
+ */
+ #maybeUntrimUrl() {
+ // Check if we can untrim the current value.
+ if (
+ !lazy.UrlbarPrefs.get("untrimOnUserInteraction.featureGate") ||
+ !this._protocolIsTrimmed ||
+ !this.focused ||
+ this.#allTextSelected
+ ) {
+ return;
+ }
+
+ let selectionStart = this.selectionStart;
+ let selectionEnd = this.selectionEnd;
+
+ // Correct the selection taking the trimmed protocol into account.
+ let offset = lazy.BrowserUIUtils.trimURLProtocol.length;
+
+ // In case of autofill, we may have to adjust its boundaries.
+ if (this._autofillPlaceholder) {
+ this._autofillPlaceholder.selectionStart += offset;
+ this._autofillPlaceholder.selectionEnd += offset;
+ }
+
+ if (selectionStart == selectionEnd) {
+ // When cursor is at the end of the string, untrimming may
+ // reintroduced a trailing slash and we want to move past it.
+ if (selectionEnd == this.value.length) {
+ offset += 1;
+ }
+ selectionStart = selectionEnd += offset;
+ } else {
+ // If there's a selection, we must calculate both the initial
+ // protocol and the eventual trailing slash.
+ if (selectionStart != 0) {
+ selectionStart += offset;
+ }
+ if (selectionEnd == this.value.length) {
+ offset += 1;
+ }
+ selectionEnd += offset;
+ }
+
+ this._setValue(this._untrimmedValue, {
+ allowTrim: false,
+ valueIsTyped: this.valueIsTyped,
+ });
+
+ this.setSelectionRange(selectionStart, selectionEnd);
+ }
+
// The strip-on-share feature will strip known tracking/decorational
// query params from the URI and copy the stripped version to the clipboard.
_initStripOnShare() {
@@ -3152,8 +3271,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;
});
@@ -3326,7 +3445,7 @@ export class UrlbarInput {
if (
!this._preventClickSelectsAll &&
this._compositionState != lazy.UrlbarUtils.COMPOSITION.COMPOSING &&
- this.document.activeElement == this.inputField &&
+ this.focused &&
this.inputField.selectionStart == this.inputField.selectionEnd
) {
this.select();
@@ -3376,14 +3495,17 @@ export class UrlbarInput {
// If we were autofilling, remove the autofilled portion, by restoring
// the value to the last typed one.
this.value = this.window.gBrowser.userTypedValue;
- } else if (this.value == this._focusUntrimmedValue) {
+ } else if (
+ this.value == this._untrimmedValue &&
+ !this.window.gBrowser.userTypedValue &&
+ !this.focused
+ ) {
// If the value was untrimmed by _on_focus and didn't change, trim it.
- this.value = this._focusUntrimmedValue;
+ this.value = this._untrimmedValue;
} else {
// We're not updating the value, so just format it.
this.formatValue();
}
- this._focusUntrimmedValue = null;
this._revertOnBlurValue = null;
this._resetSearchState();
@@ -3441,6 +3563,7 @@ export class UrlbarInput {
event.target.id == SEARCH_BUTTON_ID
) {
this._maybeSelectAll();
+ this.#maybeUntrimUrl();
}
if (event.target == this._searchModeIndicatorClose && event.button != 2) {
@@ -3482,7 +3605,7 @@ export class UrlbarInput {
// This is necessary when a protocol was typed, but the whole url has
// invalid parts, like the origin, then editing and confirming the trimmed
// value would execute a search instead of visiting the typed url.
- if (this.value != this._untrimmedValue) {
+ if (this._protocolIsTrimmed) {
let untrim = false;
let fixedURI = this._getURIFixupInfo(this.value)?.preferredURI;
if (fixedURI) {
@@ -3503,8 +3626,7 @@ export class UrlbarInput {
}
}
if (untrim) {
- this._focusUntrimmedValue = this._untrimmedValue;
- this._setValue(this._focusUntrimmedValue, false);
+ this._setValue(this._untrimmedValue);
}
}
@@ -3634,6 +3756,7 @@ export class UrlbarInput {
let value = this.value;
this.valueIsTyped = true;
this._untrimmedValue = value;
+ this._protocolIsTrimmed = false;
this._resultForCurrentValue = null;
this.window.gBrowser.userTypedValue = value;
@@ -3930,6 +4053,7 @@ export class UrlbarInput {
}
_on_keydown(event) {
+ this.#allTextSelectedOnKeyDown = this.#allTextSelected;
if (event.keyCode === KeyEvent.DOM_VK_RETURN) {
if (this._keyDownEnterDeferred) {
this._keyDownEnterDeferred.reject();
@@ -3957,6 +4081,9 @@ export class UrlbarInput {
}
async _on_keyup(event) {
+ if (this.#allTextSelectedOnKeyDown) {
+ this.#maybeUntrimUrl();
+ }
if (event.keyCode === KeyEvent.DOM_VK_CONTROL) {
this._isKeyDownWithCtrl = false;
}
@@ -4061,8 +4188,7 @@ export class UrlbarInput {
// Only customize the drag data if the entire value is selected and it's a
// loaded URI. Use default behavior otherwise.
if (
- this.selectionStart != 0 ||
- this.selectionEnd != this.inputField.textLength ||
+ !this.#allTextSelected ||
this.getAttribute("pageproxystate") != "valid"
) {
return;
@@ -4123,6 +4249,11 @@ export class UrlbarInput {
this._initStripOnShare();
}
+ #allTextSelectedOnKeyDown = false;
+ get #allTextSelected() {
+ return this.selectionStart == 0 && this.selectionEnd == this.value.length;
+ }
+
/**
* @param {string} value A untrimmed address bar input.
* @returns {boolean}
@@ -4143,6 +4274,12 @@ export class UrlbarInput {
.nonWebControlledBlankURI
);
}
+
+ /**
+ * Tracks a state object per browser.
+ * TODO: Merge _searchModesByBrowser into this.
+ */
+ #browserStates = new WeakMap();
}
/**
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
index 022d0b1c7c..264d86a3f4 100644
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
@@ -65,11 +65,11 @@ 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.
- ["contextualSearch.enabled", false],
+ ["contextualSearch.enabled", true],
// Whether using `ctrl` when hitting return/enter in the URL bar
// (or clicking 'go') should prefix 'www.' and suffix
@@ -178,7 +178,7 @@ const PREF_URLBAR_DEFAULTS = new Map([
// If disabled, QuickActions will not be included in either the default search
// mode or the QuickActions search mode.
- ["quickactions.enabled", false],
+ ["quickactions.enabled", true],
// Whether we will match QuickActions within a phrase and not only a prefix.
["quickactions.matchInPhrase", true],
@@ -188,9 +188,6 @@ const PREF_URLBAR_DEFAULTS = new Map([
// zero prefix state.
["quickactions.minimumSearchString", 3],
- // Show multiple actions in a random order.
- ["quickactions.randomOrderActions", false],
-
// Whether we show the Actions section in about:preferences.
["quickactions.showPrefs", false],
@@ -322,11 +319,13 @@ const PREF_URLBAR_DEFAULTS = new Map([
// homepage is opened.
["searchTips.test.ignoreShowLimits", false],
+ // Feature gate pref for secondary actions being shown in the urlbar.
+ ["secondaryActions.featureGate", false],
+
// Whether to show each local search shortcut button in the view.
["shortcuts.bookmarks", true],
["shortcuts.tabs", true],
["shortcuts.history", true],
- ["shortcuts.quickactions", false],
// Boolean to determine if the providers defined in `exposureResults`
// should be displayed in search results. This can be set by a
@@ -464,6 +463,10 @@ const PREF_URLBAR_DEFAULTS = new Map([
// The index where we show unit conversion results.
["unitConversion.suggestedIndex", 1],
+ // Untrim url, when urlbar is focused.
+ // Note: This pref will be removed once the feature is stable.
+ ["untrimOnUserInteraction.featureGate", false],
+
// Controls the empty search behavior in Search Mode:
// 0 - Show nothing
// 1 - Show search history
@@ -1507,12 +1510,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..2ed1ee4444 100644
--- a/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderAutofill.sys.mjs
@@ -68,7 +68,7 @@ const SQL_AUTOFILL_FRECENCY_THRESHOLD = `host_frecency >= (
)`;
function originQuery(where) {
- // `frecency`, `bookmarked` and `visited` are partitioned by the fixed host,
+ // `frecency`, `n_bookmarks` and `visited` are partitioned by the fixed host,
// without `www.`. `host_prefix` instead is partitioned by full host, because
// we assume a prefix may not work regardless of `www.`.
let selectVisited = where.includes("visited")
@@ -78,7 +78,7 @@ function originQuery(where) {
: "0";
let selectTitle;
let joinBookmarks;
- if (where.includes("bookmarked")) {
+ if (where.includes("n_bookmarks")) {
selectTitle = "ifnull(b.title, iif(h.frecency <> 0, h.title, NULL))";
joinBookmarks = "LEFT JOIN moz_bookmarks b ON b.fk = h.id";
} else {
@@ -87,7 +87,7 @@ function originQuery(where) {
}
return `/* do not warn (bug no): cannot use an index to sort */
${SQL_AUTOFILL_WITH},
- origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, bookmarked, visited) AS (
+ origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, n_bookmarks, visited) AS (
SELECT
id,
prefix,
@@ -96,11 +96,11 @@ function originQuery(where) {
),
host,
fixup_url(host),
- IFNULL(total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)), 0.0),
+ total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)),
${ORIGIN_FRECENCY_FIELD},
- MAX(EXISTS(
- SELECT 1 FROM moz_places WHERE origin_id = o.id AND foreign_count > 0
- )) OVER (PARTITION BY fixup_url(host)),
+ total(
+ (SELECT total(foreign_count) FROM moz_places WHERE origin_id = o.id)
+ ) OVER (PARTITION BY fixup_url(host)),
${selectVisited}
FROM moz_origins o
WHERE prefix NOT IN ('about:', 'place:')
@@ -112,7 +112,7 @@ function originQuery(where) {
ifnull(:prefix, host_prefix) || host || '/'
FROM origins
${where}
- ORDER BY frecency DESC, prefix = "https://" DESC, id DESC
+ ORDER BY frecency DESC, n_bookmarks DESC, prefix = "https://" DESC, id DESC
LIMIT 1
),
matched_place(host_fixed, url, id, title, frecency) AS (
@@ -157,11 +157,11 @@ function urlQuery(where1, where2, isBookmarkContained) {
joinBookmarks = "";
}
return `/* do not warn (bug no): cannot use an index to sort */
- WITH matched_url(url, title, frecency, bookmarked, visited, stripped_url, is_exact_match, id) AS (
+ WITH matched_url(url, title, frecency, n_bookmarks, visited, stripped_url, is_exact_match, id) AS (
SELECT url,
title,
frecency,
- foreign_count > 0 AS bookmarked,
+ foreign_count AS n_bookmarks,
visit_count > 0 AS visited,
strip_prefix_and_userinfo(url) AS stripped_url,
strip_prefix_and_userinfo(url) = strip_prefix_and_userinfo(:strippedURL) AS is_exact_match,
@@ -173,7 +173,7 @@ function urlQuery(where1, where2, isBookmarkContained) {
SELECT url,
title,
frecency,
- foreign_count > 0 AS bookmarked,
+ foreign_count AS n_bookmarks,
visit_count > 0 AS visited,
strip_prefix_and_userinfo(url) AS stripped_url,
strip_prefix_and_userinfo(url) = 'www.' || strip_prefix_and_userinfo(:strippedURL) AS is_exact_match,
@@ -196,12 +196,12 @@ function urlQuery(where1, where2, isBookmarkContained) {
// Queries
const QUERY_ORIGIN_HISTORY_BOOKMARK = originQuery(
- `WHERE bookmarked OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
+ `WHERE n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
);
const QUERY_ORIGIN_PREFIX_HISTORY_BOOKMARK = originQuery(
`WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF'
- AND (bookmarked OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`
+ AND (n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`
);
const QUERY_ORIGIN_HISTORY = originQuery(
@@ -213,38 +213,38 @@ const QUERY_ORIGIN_PREFIX_HISTORY = originQuery(
AND visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`
);
-const QUERY_ORIGIN_BOOKMARK = originQuery(`WHERE bookmarked`);
+const QUERY_ORIGIN_BOOKMARK = originQuery(`WHERE n_bookmarks > 0`);
const QUERY_ORIGIN_PREFIX_BOOKMARK = originQuery(
- `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF' AND bookmarked`
+ `WHERE prefix BETWEEN :prefix AND :prefix || X'FFFF' AND n_bookmarks > 0`
);
const QUERY_URL_HISTORY_BOOKMARK = urlQuery(
- `AND (bookmarked OR frecency > 20)
+ `AND (n_bookmarks > 0 OR frecency > 20)
AND stripped_url COLLATE NOCASE
BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
- `AND (bookmarked OR frecency > 20)
+ `AND (n_bookmarks > 0 OR frecency > 20)
AND stripped_url COLLATE NOCASE
BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
true
);
const QUERY_URL_PREFIX_HISTORY_BOOKMARK = urlQuery(
- `AND (bookmarked OR frecency > 20)
+ `AND (n_bookmarks > 0 OR frecency > 20)
AND url COLLATE NOCASE
BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
- `AND (bookmarked OR frecency > 20)
+ `AND (n_bookmarks > 0 OR frecency > 20)
AND url COLLATE NOCASE
BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
true
);
const QUERY_URL_HISTORY = urlQuery(
- `AND (visited OR NOT bookmarked)
+ `AND (visited OR n_bookmarks = 0)
AND frecency > 20
AND stripped_url COLLATE NOCASE
BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
- `AND (visited OR NOT bookmarked)
+ `AND (visited OR n_bookmarks = 0)
AND frecency > 20
AND stripped_url COLLATE NOCASE
BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
@@ -252,11 +252,11 @@ const QUERY_URL_HISTORY = urlQuery(
);
const QUERY_URL_PREFIX_HISTORY = urlQuery(
- `AND (visited OR NOT bookmarked)
+ `AND (visited OR n_bookmarks = 0)
AND frecency > 20
AND url COLLATE NOCASE
BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
- `AND (visited OR NOT bookmarked)
+ `AND (visited OR n_bookmarks = 0)
AND frecency > 20
AND url COLLATE NOCASE
BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
@@ -264,20 +264,20 @@ const QUERY_URL_PREFIX_HISTORY = urlQuery(
);
const QUERY_URL_BOOKMARK = urlQuery(
- `AND bookmarked
+ `AND n_bookmarks > 0
AND stripped_url COLLATE NOCASE
BETWEEN :strippedURL AND :strippedURL || X'FFFF'`,
- `AND bookmarked
+ `AND n_bookmarks > 0
AND stripped_url COLLATE NOCASE
BETWEEN 'www.' || :strippedURL AND 'www.' || :strippedURL || X'FFFF'`,
true
);
const QUERY_URL_PREFIX_BOOKMARK = urlQuery(
- `AND bookmarked
+ `AND n_bookmarks > 0
AND url COLLATE NOCASE
BETWEEN :prefix || :strippedURL AND :prefix || :strippedURL || X'FFFF'`,
- `AND bookmarked
+ `AND n_bookmarks > 0
AND url COLLATE NOCASE
BETWEEN :prefix || 'www.' || :strippedURL AND :prefix || 'www.' || :strippedURL || X'FFFF'`,
true
@@ -452,17 +452,19 @@ class ProviderAutofill extends UrlbarProvider {
sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY) &&
sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)
) {
- conditions.push(`(bookmarked OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`);
+ conditions.push(
+ `(n_bookmarks > 0 OR ${SQL_AUTOFILL_FRECENCY_THRESHOLD})`
+ );
} else if (sources.includes(UrlbarUtils.RESULT_SOURCE.HISTORY)) {
conditions.push(`visited AND ${SQL_AUTOFILL_FRECENCY_THRESHOLD}`);
} else if (sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS)) {
- conditions.push("bookmarked");
+ conditions.push("n_bookmarks > 0");
}
let rows = await db.executeCached(
`
${SQL_AUTOFILL_WITH},
- origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, bookmarked, visited) AS (
+ origins(id, prefix, host_prefix, host, fixed, host_frecency, frecency, n_bookmarks, visited) AS (
SELECT
id,
prefix,
@@ -471,11 +473,11 @@ class ProviderAutofill extends UrlbarProvider {
),
host,
fixup_url(host),
- IFNULL(total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)), 0.0),
+ total(${ORIGIN_FRECENCY_FIELD}) OVER (PARTITION BY fixup_url(host)),
${ORIGIN_FRECENCY_FIELD},
- MAX(EXISTS(
- SELECT 1 FROM moz_places WHERE origin_id = o.id AND foreign_count > 0
- )) OVER (PARTITION BY fixup_url(host)),
+ total(
+ (SELECT total(foreign_count) FROM moz_places WHERE origin_id = o.id)
+ ) OVER (PARTITION BY fixup_url(host)),
MAX(EXISTS(
SELECT 1 FROM moz_places WHERE origin_id = o.id AND visit_count > 0
)) OVER (PARTITION BY fixup_url(host))
@@ -653,7 +655,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
deleted file mode 100644
index 63c94ee8f3..0000000000
--- a/browser/components/urlbar/UrlbarProviderContextualSearch.sys.mjs
+++ /dev/null
@@ -1,280 +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 {
- UrlbarProvider,
- UrlbarUtils,
-} from "resource:///modules/UrlbarUtils.sys.mjs";
-
-const lazy = {};
-
-ChromeUtils.defineESModuleGetters(lazy, {
- BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
- OpenSearchEngine: "resource://gre/modules/OpenSearchEngine.sys.mjs",
- loadAndParseOpenSearchEngine:
- "resource://gre/modules/OpenSearchLoader.sys.mjs",
- UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
- UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
- UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
- UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
-});
-
-const DYNAMIC_RESULT_TYPE = "contextualSearch";
-
-const ENABLED_PREF = "contextualSearch.enabled";
-
-const VIEW_TEMPLATE = {
- attributes: {
- selectable: true,
- },
- children: [
- {
- name: "no-wrap",
- tag: "span",
- classList: ["urlbarView-no-wrap", "urlbarView-overflowable"],
- children: [
- {
- name: "icon",
- tag: "img",
- classList: ["urlbarView-favicon"],
- },
- {
- name: "search",
- tag: "span",
- classList: ["urlbarView-title", "urlbarView-overflowable"],
- },
- {
- name: "separator",
- tag: "span",
- classList: ["urlbarView-title-separator"],
- },
- {
- name: "description",
- tag: "span",
- },
- ],
- },
- ],
-};
-
-/**
- * A provider that returns an option for using the search engine provided
- * by the active view if it utilizes OpenSearch.
- */
-class ProviderContextualSearch extends UrlbarProvider {
- constructor() {
- super();
- this.engines = new Map();
- lazy.UrlbarResult.addDynamicResultType(DYNAMIC_RESULT_TYPE);
- lazy.UrlbarView.addDynamicViewTemplate(DYNAMIC_RESULT_TYPE, VIEW_TEMPLATE);
- }
-
- /**
- * Unique name for the provider, used by the context to filter on providers.
- * Not using a unique name will cause the newest registration to win.
- *
- * @returns {string}
- */
- get name() {
- return "UrlbarProviderContextualSearch";
- }
-
- /**
- * The type of the provider.
- *
- * @returns {UrlbarUtils.PROVIDER_TYPE}
- */
- get type() {
- return UrlbarUtils.PROVIDER_TYPE.PROFILE;
- }
-
- /**
- * Whether this provider should be invoked for the given context.
- * If this method returns false, the providers manager won't start a query
- * with this provider, to save on resources.
- *
- * @param {UrlbarQueryContext} queryContext The query context object
- * @returns {boolean} Whether this provider should be invoked for the search.
- */
- isActive(queryContext) {
- return (
- queryContext.trimmedSearchString &&
- !queryContext.searchMode &&
- lazy.UrlbarPrefs.get(ENABLED_PREF)
- );
- }
-
- /**
- * Starts querying. Extended classes should return a Promise resolved when the
- * provider is done searching AND returning results.
- *
- * @param {UrlbarQueryContext} queryContext The query context object
- * @param {Function} addCallback Callback invoked by the provider to add a new
- * result. A UrlbarResult should be passed to it.
- */
- async startQuery(queryContext, addCallback) {
- let engine;
- const hostname =
- queryContext?.currentPage && new URL(queryContext.currentPage).hostname;
-
- // This happens on about pages, which won't have associated engines
- if (!hostname) {
- return;
- }
-
- // First check to see if there's a cached search engine for the host.
- // If not, check to see if an installed engine matches the current view.
- if (this.engines.has(hostname)) {
- engine = this.engines.get(hostname);
- } else {
- // Strip www. to allow for partial matches when looking for an engine.
- const [host] = UrlbarUtils.stripPrefixAndTrim(hostname, {
- stripWww: true,
- });
- engine = (
- await lazy.UrlbarSearchUtils.enginesForDomainPrefix(host, {
- matchAllDomainLevels: true,
- })
- )[0];
- }
-
- if (engine) {
- let instance = this.queryInstance;
- let icon = await engine.getIconURL();
- if (instance != this.queryInstance) {
- return;
- }
-
- this.engines.set(hostname, engine);
- // Check to see if the engine that was found is the default engine.
- // The default engine will often be used to populate the heuristic result,
- // and we want to avoid ending up with two nearly identical search results.
- let defaultEngine = lazy.UrlbarSearchUtils.getDefaultEngine();
- if (engine.name === defaultEngine?.name) {
- return;
- }
- const [url] = UrlbarUtils.getSearchQueryUrl(
- engine,
- queryContext.searchString
- );
- let result = this.makeResult({
- url,
- engine: engine.name,
- icon,
- input: queryContext.searchString,
- shouldNavigate: true,
- });
- addCallback(this, result);
- return;
- }
-
- // If the current view has engines that haven't been added, return a result
- // that will first add an engine, then use it to search.
- let window = lazy.BrowserWindowTracker.getTopWindow();
- let engineToAdd = window?.gBrowser.selectedBrowser?.engines?.[0];
-
- if (engineToAdd) {
- let result = this.makeResult({
- hostname,
- url: engineToAdd.uri,
- engine: engineToAdd.title,
- icon: engineToAdd.icon,
- input: queryContext.searchString,
- shouldAddEngine: true,
- });
- addCallback(this, result);
- }
- }
-
- makeResult({
- engine,
- icon,
- url,
- input,
- hostname,
- shouldNavigate = false,
- shouldAddEngine = false,
- }) {
- let result = new lazy.UrlbarResult(
- UrlbarUtils.RESULT_TYPE.DYNAMIC,
- UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
- {
- engine,
- icon,
- url,
- input,
- hostname,
- shouldAddEngine,
- shouldNavigate,
- dynamicType: DYNAMIC_RESULT_TYPE,
- }
- );
- result.suggestedIndex = -1;
- return result;
- }
-
- /**
- * This is called when the urlbar view updates the view of one of the results
- * of the provider. It should return an object describing the view update.
- * See the base UrlbarProvider class for more.
- *
- * @param {UrlbarResult} result The result whose view will be updated.
- * @returns {object} An object describing the view update.
- */
- getViewUpdate(result) {
- return {
- icon: {
- attributes: {
- src: result.payload.icon || UrlbarUtils.ICON.SEARCH_GLASS,
- },
- },
- search: {
- textContent: result.payload.input,
- attributes: {
- title: result.payload.input,
- },
- },
- description: {
- l10n: {
- id: "urlbar-result-action-search-w-engine",
- args: {
- engine: result.payload.engine,
- },
- },
- },
- };
- }
-
- onEngagement(state, queryContext, details, controller) {
- let { result } = details;
- if (result?.providerName == this.name) {
- this.#pickResult(result, controller.browserWindow);
- }
- }
-
- async #pickResult(result, window) {
- // If we have an engine to add, first create a new OpenSearchEngine, then
- // get and open a url to execute a search for the term in the url bar.
- // In cases where we don't have to create a new engine, navigation is
- // handled automatically by providing `shouldNavigate: true` in the result.
- if (result.payload.shouldAddEngine) {
- let engineData = await lazy.loadAndParseOpenSearchEngine(
- Services.io.newURI(result.payload.url)
- );
- let newEngine = new lazy.OpenSearchEngine({ engineData });
- newEngine._setIcon(result.payload.icon, false);
- this.engines.set(result.payload.hostname, newEngine);
- const [url] = UrlbarUtils.getSearchQueryUrl(
- newEngine,
- result.payload.input
- );
- window.gBrowser.fixupAndLoadURIString(url, {
- triggeringPrincipal:
- Services.scriptSecurityManager.getSystemPrincipal(),
- });
- }
- }
-}
-
-export var UrlbarProviderContextualSearch = new ProviderContextualSearch();
diff --git a/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs b/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs
index f929a1c003..a259d639cc 100644
--- a/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderInputHistory.sys.mjs
@@ -144,15 +144,25 @@ class ProviderInputHistory extends UrlbarProvider {
// Don't suggest switching to the current page.
continue;
}
- let result = new lazy.UrlbarResult(
- UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
- UrlbarUtils.RESULT_SOURCE.TABS,
- ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
+ let payload = lazy.UrlbarResult.payloadAndSimpleHighlights(
+ queryContext.tokens,
+ {
url: [url, UrlbarUtils.HIGHLIGHT.TYPED],
title: [resultTitle, UrlbarUtils.HIGHLIGHT.TYPED],
icon: UrlbarUtils.getIconForUrl(url),
userContextId: row.getResultByName("userContextId") || 0,
- })
+ }
+ );
+ if (lazy.UrlbarPrefs.get("secondaryActions.featureGate")) {
+ payload[0].action = {
+ key: "tabswitch",
+ l10nId: "urlbar-result-action-switch-tab",
+ };
+ }
+ let result = new lazy.UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ UrlbarUtils.RESULT_SOURCE.TABS,
+ ...payload
);
addCallback(this, result);
continue;
@@ -200,7 +210,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 +246,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..0787d4c209 100644
--- a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
@@ -330,17 +330,25 @@ function makeUrlbarResult(tokens, info) {
action.params.searchSuggestion.toLocaleLowerCase(),
})
);
- case "switchtab":
+ case "switchtab": {
+ let payload = lazy.UrlbarResult.payloadAndSimpleHighlights(tokens, {
+ url: [action.params.url, UrlbarUtils.HIGHLIGHT.TYPED],
+ title: [info.comment, UrlbarUtils.HIGHLIGHT.TYPED],
+ icon: info.icon,
+ userContextId: info.userContextId,
+ });
+ if (lazy.UrlbarPrefs.get("secondaryActions.featureGate")) {
+ payload[0].action = {
+ key: "tabswitch",
+ l10nId: "urlbar-result-action-switch-tab",
+ };
+ }
return new lazy.UrlbarResult(
UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
UrlbarUtils.RESULT_SOURCE.TABS,
- ...lazy.UrlbarResult.payloadAndSimpleHighlights(tokens, {
- url: [action.params.url, UrlbarUtils.HIGHLIGHT.TYPED],
- title: [info.comment, UrlbarUtils.HIGHLIGHT.TYPED],
- icon: info.icon,
- userContextId: info.userContextId,
- })
+ ...payload
);
+ }
case "visiturl":
return new lazy.UrlbarResult(
UrlbarUtils.RESULT_TYPE.URL,
@@ -1517,7 +1525,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
deleted file mode 100644
index f199b6b892..0000000000
--- a/browser/components/urlbar/UrlbarProviderQuickActions.sys.mjs
+++ /dev/null
@@ -1,357 +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 {
- UrlbarProvider,
- UrlbarUtils,
-} from "resource:///modules/UrlbarUtils.sys.mjs";
-
-const lazy = {};
-ChromeUtils.defineESModuleGetters(lazy, {
- QuickActionsLoaderDefault:
- "resource:///modules/QuickActionsLoaderDefault.sys.mjs",
- UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
- UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
-});
-
-// These prefs are relative to the `browser.urlbar` branch.
-const ENABLED_PREF = "quickactions.enabled";
-const SUGGEST_PREF = "suggest.quickactions";
-const MATCH_IN_PHRASE_PREF = "quickactions.matchInPhrase";
-const MIN_SEARCH_PREF = "quickactions.minimumSearchString";
-const DYNAMIC_TYPE_NAME = "quickactions";
-
-// When the urlbar is first focused and no search term has been
-// entered we show a limited number of results.
-const ACTIONS_SHOWN_FOCUS = 4;
-
-// Default icon shown for actions if no custom one is provided.
-const DEFAULT_ICON = "chrome://global/skin/icons/settings.svg";
-
-// The suggestion index of the actions row within the urlbar results.
-const SUGGESTED_INDEX = 1;
-
-/**
- * A provider that returns a suggested url to the user based on what
- * they have currently typed so they can navigate directly.
- */
-class ProviderQuickActions extends UrlbarProvider {
- constructor() {
- super();
- lazy.UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME);
- }
-
- /**
- * Returns the name of this provider.
- *
- * @returns {string} the name of this provider.
- */
- get name() {
- return DYNAMIC_TYPE_NAME;
- }
-
- /**
- * The type of the provider.
- *
- * @returns {UrlbarUtils.PROVIDER_TYPE}
- */
- get type() {
- return UrlbarUtils.PROVIDER_TYPE.PROFILE;
- }
-
- getPriority(context) {
- if (!context.searchString) {
- return 1;
- }
- return 0;
- }
-
- /**
- * Whether this provider should be invoked for the given context.
- * If this method returns false, the providers manager won't start a query
- * with this provider, to save on resources.
- *
- * @param {UrlbarQueryContext} queryContext The query context object
- * @returns {boolean} Whether this provider should be invoked for the search.
- */
- isActive(queryContext) {
- return (
- queryContext.trimmedSearchString.length < 50 &&
- lazy.UrlbarPrefs.get(ENABLED_PREF) &&
- ((lazy.UrlbarPrefs.get(SUGGEST_PREF) && !queryContext.searchMode) ||
- queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.ACTIONS)
- );
- }
-
- /**
- * Starts querying. Extended classes should return a Promise resolved when the
- * provider is done searching AND returning results.
- *
- * @param {UrlbarQueryContext} queryContext The query context object
- * @param {Function} addCallback Callback invoked by the provider to add a new
- * result. A UrlbarResult should be passed to it.
- * @returns {Promise}
- */
- async startQuery(queryContext, addCallback) {
- await lazy.QuickActionsLoaderDefault.ensureLoaded();
- let input = queryContext.trimmedSearchString.toLowerCase();
-
- if (
- !queryContext.searchMode &&
- input.length < lazy.UrlbarPrefs.get(MIN_SEARCH_PREF)
- ) {
- return;
- }
-
- let results = [...(this.#prefixes.get(input) ?? [])];
-
- if (lazy.UrlbarPrefs.get(MATCH_IN_PHRASE_PREF)) {
- for (let [keyword, key] of this.#keywords) {
- if (input.includes(keyword)) {
- results.push(key);
- }
- }
- }
- // Ensure results are unique.
- results = [...new Set(results)];
-
- // Remove invisible actions.
- results = results.filter(key => {
- const action = this.#actions.get(key);
- return !action.isVisible || action.isVisible();
- });
-
- if (!results?.length) {
- return;
- }
-
- // If all actions are inactive, don't show anything.
- if (
- results.every(key => {
- const action = this.#actions.get(key);
- return action.isActive && !action.isActive();
- })
- ) {
- return;
- }
-
- // If we are in the Actions searchMode then we want to show all the actions
- // but not when we are in the normal url mode on first focus.
- if (
- results.length > ACTIONS_SHOWN_FOCUS &&
- !input &&
- !queryContext.searchMode
- ) {
- results.length = ACTIONS_SHOWN_FOCUS;
- }
-
- const result = new lazy.UrlbarResult(
- UrlbarUtils.RESULT_TYPE.DYNAMIC,
- UrlbarUtils.RESULT_SOURCE.ACTIONS,
- {
- results: results.map(key => ({ key })),
- dynamicType: DYNAMIC_TYPE_NAME,
- inputLength: input.length,
- inQuickActionsSearchMode:
- queryContext.searchMode?.source == UrlbarUtils.RESULT_SOURCE.ACTIONS,
- }
- );
- result.suggestedIndex = SUGGESTED_INDEX;
- addCallback(this, result);
- this.#resultFromLastQuery = result;
- }
-
- getViewTemplate(result) {
- return {
- children: [
- {
- name: "buttons",
- tag: "div",
- attributes: {
- "data-is-quickactions-searchmode":
- result.payload.inQuickActionsSearchMode,
- },
- children: result.payload.results.map(({ key }, i) => {
- let action = this.#actions.get(key);
- let inActive = "isActive" in action && !action.isActive();
- return {
- name: `button-${i}`,
- tag: "span",
- attributes: {
- "data-key": key,
- "data-input-length": result.payload.inputLength,
- class: "urlbarView-quickaction-button",
- role: inActive ? "" : "button",
- disabled: inActive,
- },
- children: [
- {
- name: `icon-${i}`,
- tag: "div",
- attributes: { class: "urlbarView-favicon" },
- children: [
- {
- name: `image-${i}`,
- tag: "img",
- attributes: {
- class: "urlbarView-favicon-img",
- src: action.icon || DEFAULT_ICON,
- },
- },
- ],
- },
- {
- name: `label-${i}`,
- tag: "span",
- attributes: { class: "urlbarView-label" },
- },
- ],
- };
- }),
- },
- ],
- };
- }
-
- getViewUpdate(result) {
- let viewUpdate = {};
- result.payload.results.forEach(({ key }, i) => {
- let action = this.#actions.get(key);
- viewUpdate[`label-${i}`] = {
- l10n: { id: action.label, cacheable: true },
- };
- });
- return viewUpdate;
- }
-
- #pickResult(result, itemPicked) {
- let { key, inputLength } = itemPicked.dataset;
- // We clamp the input length to limit the number of keys to
- // the number of actions * 10.
- inputLength = Math.min(inputLength, 10);
- Services.telemetry.keyedScalarAdd(
- `quickaction.picked`,
- `${key}-${inputLength}`,
- 1
- );
- let options = this.#actions.get(itemPicked.dataset.key).onPick() ?? {};
- if (options.focusContent) {
- itemPicked.ownerGlobal.gBrowser.selectedBrowser.focus();
- }
- }
-
- onEngagement(state, queryContext, details, controller) {
- // Ignore engagements on other results that didn't end the session.
- if (details.result?.providerName != this.name && details.isSessionOngoing) {
- return;
- }
-
- if (state == "engagement" && queryContext) {
- // Get the result that's visible in the view. `details.result` is the
- // engaged result, if any; if it's from this provider, then that's the
- // visible result. Otherwise fall back to #getVisibleResultFromLastQuery.
- let { result } = details;
- if (result?.providerName != this.name) {
- result = this.#getVisibleResultFromLastQuery(controller.view);
- }
-
- result?.payload.results.forEach(({ key }) => {
- Services.telemetry.keyedScalarAdd(
- `quickaction.impression`,
- `${key}-${queryContext.trimmedSearchString.length}`,
- 1
- );
- });
- }
-
- // Handle picks.
- if (details.result?.providerName == this.name) {
- this.#pickResult(details.result, details.element);
- }
-
- this.#resultFromLastQuery = null;
- }
-
- /**
- * Adds a new QuickAction.
- *
- * @param {string} key A key to identify this action.
- * @param {string} definition An object that describes the action.
- */
- addAction(key, definition) {
- this.#actions.set(key, definition);
- definition.commands.forEach(cmd => this.#keywords.set(cmd, key));
- this.#loopOverPrefixes(definition.commands, prefix => {
- let result = this.#prefixes.get(prefix);
- if (result) {
- if (!result.includes(key)) {
- result.push(key);
- }
- } else {
- result = [key];
- }
- this.#prefixes.set(prefix, result);
- });
- }
-
- /**
- * Removes an action.
- *
- * @param {string} key A key to identify this action.
- */
- removeAction(key) {
- let definition = this.#actions.get(key);
- this.#actions.delete(key);
- definition.commands.forEach(cmd => this.#keywords.delete(cmd));
- this.#loopOverPrefixes(definition.commands, prefix => {
- let result = this.#prefixes.get(prefix);
- if (result) {
- result = result.filter(val => val != key);
- }
- this.#prefixes.set(prefix, result);
- });
- }
-
- // A map from keywords to an action.
- #keywords = new Map();
-
- // A map of all prefixes to an array of actions.
- #prefixes = new Map();
-
- // The actions that have been added.
- #actions = new Map();
-
- // The result we added during the most recent query.
- #resultFromLastQuery = null;
-
- #loopOverPrefixes(commands, fun) {
- for (const command of commands) {
- // Loop over all the prefixes of the word, ie
- // "", "w", "wo", "wor", stopping just before the full
- // word itself which will be matched by the whole
- // phrase matching.
- for (let i = 1; i <= command.length; i++) {
- let prefix = command.substring(0, command.length - i);
- fun(prefix);
- }
- }
- }
-
- #getVisibleResultFromLastQuery(view) {
- let result = this.#resultFromLastQuery;
-
- if (
- result?.rowIndex >= 0 &&
- view?.visibleResults?.[result.rowIndex] == result
- ) {
- // The result was visible.
- return result;
- }
-
- // Find a visible result.
- return view?.visibleResults?.find(r => r.providerName == this.name);
- }
-}
-
-export var UrlbarProviderQuickActions = new ProviderQuickActions();
diff --git a/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs b/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs
index 78e254616e..10244e06b5 100644
--- a/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs
@@ -45,7 +45,6 @@ const TELEMETRY_SCALARS = {
CLICK_NAV_SUPERCEDED: `${TELEMETRY_PREFIX}.click_nav_superceded`,
CLICK_NONSPONSORED: `${TELEMETRY_PREFIX}.click_nonsponsored`,
CLICK_SPONSORED: `${TELEMETRY_PREFIX}.click_sponsored`,
- HELP_DYNAMIC_WIKIPEDIA: `${TELEMETRY_PREFIX}.help_dynamic_wikipedia`,
HELP_NONSPONSORED: `${TELEMETRY_PREFIX}.help_nonsponsored`,
HELP_SPONSORED: `${TELEMETRY_PREFIX}.help_sponsored`,
IMPRESSION_DYNAMIC_WIKIPEDIA: `${TELEMETRY_PREFIX}.impression_dynamic_wikipedia`,
@@ -229,7 +228,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 +236,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();
}
@@ -413,14 +412,11 @@ class ProviderQuickSuggest extends UrlbarProvider {
let payload = {
url: suggestion.url,
isSponsored: suggestion.is_sponsored,
- 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,
};
if (suggestion.full_keyword) {
@@ -486,8 +482,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 = "";
@@ -592,9 +588,6 @@ class ProviderQuickSuggest extends UrlbarProvider {
scalars.push(TELEMETRY_SCALARS.CLICK_DYNAMIC_WIKIPEDIA);
} else {
switch (resultSelType) {
- case "help":
- scalars.push(TELEMETRY_SCALARS.HELP_DYNAMIC_WIKIPEDIA);
- break;
case "dismiss":
scalars.push(TELEMETRY_SCALARS.BLOCK_DYNAMIC_WIKIPEDIA);
break;
@@ -781,8 +774,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..6dc38e7aed 100644
--- a/browser/components/urlbar/UrlbarProviderWeather.sys.mjs
+++ b/browser/components/urlbar/UrlbarProviderWeather.sys.mjs
@@ -20,7 +20,6 @@ const TELEMETRY_PREFIX = "contextual.services.quicksuggest";
const TELEMETRY_SCALARS = {
BLOCK: `${TELEMETRY_PREFIX}.block_weather`,
CLICK: `${TELEMETRY_PREFIX}.click_weather`,
- HELP: `${TELEMETRY_PREFIX}.help_weather`,
IMPRESSION: `${TELEMETRY_PREFIX}.impression_weather`,
};
@@ -115,7 +114,7 @@ class ProviderWeather extends UrlbarProvider {
return false;
}
- return keywords.has(queryContext.searchString.trim().toLocaleLowerCase());
+ return keywords.has(queryContext.trimmedLowerCaseSearchString);
}
/**
@@ -163,7 +162,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;
@@ -233,7 +232,6 @@ class ProviderWeather extends UrlbarProvider {
*
* - "": The user didn't pick the row or any part of it
* - "weather": The user picked the main part of the row
- * - "help": The user picked the help button
* - "dismiss": The user dismissed the result
*
* An empty string means the user picked some other row to end the
@@ -243,7 +241,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
@@ -265,10 +263,6 @@ class ProviderWeather extends UrlbarProvider {
clickScalars.push(TELEMETRY_SCALARS.CLICK);
eventObject = "click";
break;
- case "help":
- clickScalars.push(TELEMETRY_SCALARS.HELP);
- eventObject = "help";
- break;
case "dismiss":
clickScalars.push(TELEMETRY_SCALARS.BLOCK);
eventObject = "block";
diff --git a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
index 609b0735e1..3fd9b0caf3 100644
--- a/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
+++ b/browser/components/urlbar/UrlbarProvidersManager.sys.mjs
@@ -40,8 +40,6 @@ var localProviderModules = {
"resource:///modules/UrlbarProviderCalculator.sys.mjs",
UrlbarProviderClipboard:
"resource:///modules/UrlbarProviderClipboard.sys.mjs",
- UrlbarProviderContextualSearch:
- "resource:///modules/UrlbarProviderContextualSearch.sys.mjs",
UrlbarProviderHeuristicFallback:
"resource:///modules/UrlbarProviderHeuristicFallback.sys.mjs",
UrlbarProviderHistoryUrlHeuristic:
@@ -54,8 +52,6 @@ var localProviderModules = {
UrlbarProviderPlaces: "resource:///modules/UrlbarProviderPlaces.sys.mjs",
UrlbarProviderPrivateSearch:
"resource:///modules/UrlbarProviderPrivateSearch.sys.mjs",
- UrlbarProviderQuickActions:
- "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
UrlbarProviderQuickSuggest:
"resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
UrlbarProviderQuickSuggestContextualOptIn:
@@ -84,6 +80,14 @@ var localMuxerModules = {
"resource:///modules/UrlbarMuxerUnifiedComplete.sys.mjs",
};
+import { ActionsProviderQuickActions } from "resource:///modules/ActionsProviderQuickActions.sys.mjs";
+import { ActionsProviderContextualSearch } from "resource:///modules/ActionsProviderContextualSearch.sys.mjs";
+
+let globalActionsProviders = [
+ ActionsProviderContextualSearch,
+ ActionsProviderQuickActions,
+];
+
const DEFAULT_MUXER = "UnifiedComplete";
/**
@@ -179,6 +183,17 @@ class ProvidersManager {
}
/**
+ * Returns the provider with the given name.
+ *
+ * @param {string} name
+ * The provider name.
+ * @returns {UrlbarProvider} The provider.
+ */
+ getActionProvider(name) {
+ return globalActionsProviders.find(p => p.name == name);
+ }
+
+ /**
* Registers a muxer object with the manager.
*
* @param {object} muxer
@@ -284,6 +299,12 @@ class ProvidersManager {
// history and bookmarks even if search engines are not available.
}
+ // All current global actions are currently memory lookups so it is safe to
+ // wait on them.
+ this.#globalAction = lazy.UrlbarPrefs.get("secondaryActions.featureGate")
+ ? await this.pickGlobalAction(queryContext, controller)
+ : null;
+
if (query.canceled) {
return;
}
@@ -334,11 +355,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 +370,7 @@ class ProvidersManager {
notifyEngagementChange(state, queryContext, details = {}, controller) {
for (let provider of this.providers) {
provider.tryMethod(
- "onEngagement",
+ "onLegacyEngagement",
state,
queryContext,
details,
@@ -357,6 +378,25 @@ class ProvidersManager {
);
}
}
+
+ #globalAction = null;
+
+ async pickGlobalAction(queryContext, controller) {
+ for (let provider of globalActionsProviders) {
+ if (provider.isActive(queryContext)) {
+ let action = await provider.queryAction(queryContext, controller);
+ if (action) {
+ action.providerName = provider.name;
+ return action;
+ }
+ }
+ }
+ return null;
+ }
+
+ getGlobalAction() {
+ return this.#globalAction;
+ }
}
export var UrlbarProvidersManager = new ProvidersManager();
diff --git a/browser/components/urlbar/UrlbarTokenizer.sys.mjs b/browser/components/urlbar/UrlbarTokenizer.sys.mjs
index c0b3a9c069..ee565ec1c9 100644
--- a/browser/components/urlbar/UrlbarTokenizer.sys.mjs
+++ b/browser/components/urlbar/UrlbarTokenizer.sys.mjs
@@ -246,7 +246,7 @@ export var UrlbarTokenizer = {
queryContext.tokens = [];
return queryContext;
}
- let unfiltered = splitString(queryContext.searchString);
+ let unfiltered = splitString(queryContext);
let tokens = filterTokens(unfiltered);
queryContext.tokens = tokens;
return queryContext;
@@ -276,13 +276,17 @@ const CHAR_TO_TYPE_MAP = new Map(
);
/**
- * Given a search string, splits it into string tokens.
+ * Given a queryContext object, splits its searchString into string tokens.
*
- * @param {string} searchString
- * The search string to split
+ * @param {UrlbarQueryContext} queryContext
+ * The query context object to tokenize.
+ * @param {string} queryContext.searchString
+ * The search string to split.
+ * @param {object} queryContext.searchMode
+ * A search mode object.
* @returns {Array} An array of string tokens.
*/
-function splitString(searchString) {
+function splitString({ searchString, searchMode }) {
// The first step is splitting on unicode whitespaces. We ignore whitespaces
// if the search string starts with "data:", to better support Web developers
// and compatiblity with other browsers.
@@ -327,7 +331,8 @@ function splitString(searchString) {
// allow for a typed question to yield only search results.
if (
CHAR_TO_TYPE_MAP.has(firstToken[0]) &&
- !UrlbarTokenizer.REGEXP_PERCENT_ENCODED_START.test(firstToken)
+ !UrlbarTokenizer.REGEXP_PERCENT_ENCODED_START.test(firstToken) &&
+ !searchMode
) {
tokens[0] = firstToken.substring(1);
tokens.splice(0, 0, firstToken[0]);
diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs
index 2bbb5d1ab0..096d1c8f2d 100644
--- a/browser/components/urlbar/UrlbarUtils.sys.mjs
+++ b/browser/components/urlbar/UrlbarUtils.sys.mjs
@@ -113,8 +113,7 @@ export var UrlbarUtils = {
TABS: 4,
OTHER_LOCAL: 5,
OTHER_NETWORK: 6,
- ACTIONS: 7,
- ADDON: 8,
+ ADDON: 7,
},
// This defines icon locations that are commonly used in the UI.
@@ -228,13 +227,6 @@ export var UrlbarUtils = {
pref: "shortcuts.history",
telemetryLabel: "history",
},
- {
- source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
- restrict: lazy.UrlbarTokenizer.RESTRICT.ACTION,
- icon: "chrome://browser/skin/quickactions.svg",
- pref: "shortcuts.quickactions",
- telemetryLabel: "actions",
- },
];
},
@@ -854,7 +846,7 @@ export var UrlbarUtils = {
* @returns {string} The modified paste data.
*/
stripUnsafeProtocolOnPaste(pasteData) {
- while (true) {
+ for (;;) {
let scheme = "";
try {
scheme = Services.io.extractScheme(pasteData);
@@ -1279,8 +1271,6 @@ export var UrlbarUtils = {
if (result.providerName == "TabToSearch") {
// This is the onboarding result.
return "tabtosearch";
- } else if (result.providerName == "quickactions") {
- return "quickaction";
} else if (result.providerName == "Weather") {
return "weather";
}
@@ -1435,14 +1425,10 @@ export var UrlbarUtils = {
switch (result.providerName) {
case "calculator":
return "calc";
- case "quickactions":
- return "action";
case "TabToSearch":
return "tab_to_search";
case "UnitConversion":
return "unit";
- case "UrlbarProviderContextualSearch":
- return "site_specific_contextual_search";
case "UrlbarProviderQuickSuggest":
return this._getQuickSuggestTelemetryType(result);
case "UrlbarProviderQuickSuggestContextualOptIn":
@@ -1535,28 +1521,6 @@ export var UrlbarUtils = {
return "unknown";
},
- /**
- * Extracts a subtype for search engagement telemetry from a result and the picked element.
- *
- * @param {UrlbarResult} result The result to analyze.
- * @param {DOMElement} element The picked view element. Nullable.
- * @returns {string} Subtype as string.
- */
- searchEngagementTelemetrySubtype(result, element) {
- if (!result) {
- return "";
- }
-
- if (
- result.providerName === "quickactions" &&
- element?.classList.contains("urlbarView-quickaction-button")
- ) {
- return element.dataset.key;
- }
-
- return "";
- },
-
_getQuickSuggestTelemetryType(result) {
if (result.payload.telemetryType == "weather") {
// Return "weather" without the usual source prefix for consistency with
@@ -1641,6 +1605,17 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
type: "object",
required: ["url"],
properties: {
+ action: {
+ type: "object",
+ properties: {
+ l10nId: {
+ type: "string",
+ },
+ key: {
+ type: "string",
+ },
+ },
+ },
displayUrl: {
type: "string",
},
@@ -1831,6 +1806,9 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
isBlockable: {
type: "boolean",
},
+ isManageable: {
+ type: "boolean",
+ },
isPinned: {
type: "boolean",
},
@@ -2175,6 +2153,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 +2411,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 +2450,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 +2468,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 +2552,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 +2565,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/UrlbarValueFormatter.sys.mjs b/browser/components/urlbar/UrlbarValueFormatter.sys.mjs
index b27bede750..2fa3d0137d 100644
--- a/browser/components/urlbar/UrlbarValueFormatter.sys.mjs
+++ b/browser/components/urlbar/UrlbarValueFormatter.sys.mjs
@@ -146,12 +146,14 @@ export class UrlbarValueFormatter {
// we can skip most of this.
if (
browser._urlMetaData &&
- browser._urlMetaData.inputValue == this.urlbarInput.untrimmedValue
+ browser._urlMetaData.inputValue == inputValue &&
+ browser._urlMetaData.untrimmedValue == this.urlbarInput.untrimmedValue
) {
return browser._urlMetaData.data;
}
browser._urlMetaData = {
- inputValue: this.urlbarInput.untrimmedValue,
+ inputValue,
+ untrimmedValue: this.urlbarInput.untrimmedValue,
data: null,
};
diff --git a/browser/components/urlbar/UrlbarView.sys.mjs b/browser/components/urlbar/UrlbarView.sys.mjs
index b5fe1e1955..c5ea040f1f 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 =>
@@ -1634,6 +1635,38 @@ export class UrlbarView {
item.appendChild(button);
}
+ #createSecondaryAction(action, global = false) {
+ let actionContainer = this.#createElement("div");
+ actionContainer.classList.add("urlbarView-actions-container");
+
+ let button = this.#createElement("span");
+ button.classList.add("urlbarView-action-btn");
+ if (global) {
+ button.classList.add("urlbarView-global-action-btn");
+ }
+ button.setAttribute("role", "button");
+ if (action.icon) {
+ let icon = this.#createElement("img");
+ icon.src = action.icon;
+ button.appendChild(icon);
+ }
+ for (let key in action.dataset ?? {}) {
+ button.dataset[key] = action.dataset[key];
+ }
+ button.dataset.action = action.key;
+ button.dataset.providerName = action.providerName;
+
+ let label = this.#createElement("span");
+ if (action.l10nId) {
+ this.#setElementL10n(label, { id: action.l10nId, args: action.l10nArgs });
+ } else {
+ this.document.l10n.setAttributes(label, action.label, action.l10nArgs);
+ }
+ button.appendChild(label);
+ actionContainer.appendChild(button);
+ return actionContainer;
+ }
+
// eslint-disable-next-line complexity
#updateRow(item, result) {
let oldResult = item.result;
@@ -1706,6 +1739,26 @@ export class UrlbarView {
}
item._content.id = item.id + "-inner";
+ let isFirstChild = item === this.#rows.children[0];
+ let secAction =
+ result.heuristic || isFirstChild
+ ? lazy.UrlbarProvidersManager.getGlobalAction()
+ : result.payload.action;
+ let container = item.querySelector(".urlbarView-actions-container");
+ if (secAction && !container) {
+ item.appendChild(this.#createSecondaryAction(secAction, isFirstChild));
+ } else if (
+ secAction &&
+ secAction.key != container.firstChild.dataset.action
+ ) {
+ item.replaceChild(
+ this.#createSecondaryAction(secAction, isFirstChild),
+ container
+ );
+ } else if (!secAction && container) {
+ item.removeChild(container);
+ }
+
item.removeAttribute("feedback-acknowledgment");
if (
@@ -1799,6 +1852,10 @@ export class UrlbarView {
let isRowSelectable = true;
switch (result.type) {
case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ // Hide chichlet when showing secondaryActions.
+ if (lazy.UrlbarPrefs.get("secondaryActions.featureGate")) {
+ break;
+ }
actionSetter = () => {
this.#setSwitchTabActionChiclet(result, action);
};
@@ -3139,6 +3196,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/docs/firefox-suggest-telemetry.rst b/browser/components/urlbar/docs/firefox-suggest-telemetry.rst
index 8d9c7c20ff..e3d37605e1 100644
--- a/browser/components/urlbar/docs/firefox-suggest-telemetry.rst
+++ b/browser/components/urlbar/docs/firefox-suggest-telemetry.rst
@@ -501,7 +501,11 @@ Changelog
Firefox 109.0
Introduced. [Bug 1800993_]
+ Firefox 127.0
+ Removed. [Bug 1891602_]
+
.. _1800993: https://bugzilla.mozilla.org/show_bug.cgi?id=1800993
+.. _1891602: https://bugzilla.mozilla.org/show_bug.cgi?id=1891602
contextual.services.quicksuggest.help_nonsponsored
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -579,7 +583,11 @@ Changelog
Firefox 110.0
Introduced. [Bug 1804536_]
+ Firefox 127.0
+ Removed. [Bug 1891602_]
+
.. _1804536: https://bugzilla.mozilla.org/show_bug.cgi?id=1804536
+.. _1891602: https://bugzilla.mozilla.org/show_bug.cgi?id=1891602
contextual.services.quicksuggest.impression
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/browser/components/urlbar/metrics.yaml b/browser/components/urlbar/metrics.yaml
index 95337d84eb..5140391e4f 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: >
@@ -455,11 +487,13 @@ urlbar:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852058
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1852058#c2
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1866204#c8
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1892377#c2
data_sensitivity:
- interaction
notification_emails:
- fx-search-telemetry@mozilla.com
- expires: 128
+ expires: 132
pref_max_results:
lifetime: application
diff --git a/browser/components/urlbar/moz.build b/browser/components/urlbar/moz.build
index e35ea11655..4b91bef331 100644
--- a/browser/components/urlbar/moz.build
+++ b/browser/components/urlbar/moz.build
@@ -12,6 +12,9 @@ DIRS += [
]
EXTRA_JS_MODULES += [
+ "ActionsProvider.sys.mjs",
+ "ActionsProviderContextualSearch.sys.mjs",
+ "ActionsProviderQuickActions.sys.mjs",
"MerinoClient.sys.mjs",
"QuickActionsLoaderDefault.sys.mjs",
"QuickSuggest.sys.mjs",
@@ -26,7 +29,6 @@ EXTRA_JS_MODULES += [
"UrlbarProviderBookmarkKeywords.sys.mjs",
"UrlbarProviderCalculator.sys.mjs",
"UrlbarProviderClipboard.sys.mjs",
- "UrlbarProviderContextualSearch.sys.mjs",
"UrlbarProviderHeuristicFallback.sys.mjs",
"UrlbarProviderHistoryUrlHeuristic.sys.mjs",
"UrlbarProviderInputHistory.sys.mjs",
@@ -35,7 +37,6 @@ EXTRA_JS_MODULES += [
"UrlbarProviderOpenTabs.sys.mjs",
"UrlbarProviderPlaces.sys.mjs",
"UrlbarProviderPrivateSearch.sys.mjs",
- "UrlbarProviderQuickActions.sys.mjs",
"UrlbarProviderQuickSuggest.sys.mjs",
"UrlbarProviderQuickSuggestContextualOptIn.sys.mjs",
"UrlbarProviderRecentSearches.sys.mjs",
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..120e7b7d0c 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;
}
}
@@ -288,7 +289,10 @@ export class SuggestBackendRust extends BaseFeature {
if (instance != this.#ingestInstance) {
return;
}
- await (this.#ingestPromise = this.#ingestHelper());
+ this.#ingestPromise = new Promise(resolve => {
+ ChromeUtils.idleDispatch(() => this.#ingestHelper().finally(resolve));
+ });
+ await this.#ingestPromise;
}
async #ingestHelper() {
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..793af24b41 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;
@@ -1043,9 +1043,11 @@ export var UrlbarTestUtils = {
* Removes the scheme from an url according to user prefs.
*
* @param {string} url
- * The url that is supposed to be sanitizied.
- * @param {{removeSingleTrailingSlash: (boolean)}} options
- * removeSingleTrailingSlash: Remove trailing slash, when trimming enabled.
+ * The url that is supposed to be trimmed.
+ * @param {object} [options]
+ * Options for the trimming.
+ * @param {boolean} [options.removeSingleTrailingSlash]
+ * Remove trailing slash, when trimming enabled.
* @returns {string}
* The sanitized URL.
*/
@@ -1060,15 +1062,13 @@ export var UrlbarTestUtils = {
lazy.BrowserUIUtils.removeSingleTrailingSlashFromURL(sanitizedURL);
}
+ // Also remove emphasis markers if present.
if (lazy.UrlbarPrefs.get("trimHttps")) {
- sanitizedURL = sanitizedURL.replace("https://", "");
+ sanitizedURL = sanitizedURL.replace(/^<?https:\/\/>?/, "");
} else {
- sanitizedURL = sanitizedURL.replace("http://", "");
+ sanitizedURL = sanitizedURL.replace(/^<?http:\/\/>?/, "");
}
- // Remove empty emphasis markers in case the protocol was trimmed.
- sanitizedURL = sanitizedURL.replace("<>", "");
-
return sanitizedURL;
},
@@ -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..38046aa26b 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",
@@ -64,6 +68,8 @@ skip-if = ["apple_catalina && debug"] # Bug 1773790
["browser_UrlbarInput_trimURLs.js"]
https_first_disabled = true
+["browser_UrlbarInput_untrimOnUserInteraction.js"]
+
["browser_aboutHomeLoading.js"]
skip-if = [
"tsan", # Intermittently times out, see 1622698 (frequent on TSan).
@@ -280,6 +286,8 @@ support-files = [
["browser_keyword_select_and_type.js"]
+["browser_less_common_selection_manipulations.js"]
+
["browser_loadRace.js"]
["browser_locationBarCommand.js"]
@@ -359,6 +367,8 @@ support-files = [
["browser_quickactions.js"]
+["browser_quickactions_commands.js"]
+
["browser_quickactions_devtools.js"]
["browser_quickactions_screenshot.js"]
@@ -398,9 +408,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,8 +497,13 @@ 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_secondaryActions.js"]
+
["browser_selectStaleResults.js"]
support-files = [
"searchSuggestionEngineSlow.xml",
@@ -639,9 +651,6 @@ tags = "search-telemetry"
https_first_disabled = true
tags = "search-telemetry"
-["browser_urlbar_telemetry_quickactions.js"]
-tags = "search-telemetry"
-
["browser_urlbar_telemetry_remotetab.js"]
tags = "search-telemetry"
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_UrlbarInput_untrimOnUserInteraction.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_untrimOnUserInteraction.js
new file mode 100644
index 0000000000..a6714df360
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_untrimOnUserInteraction.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tests = [
+ {
+ description: "Test single click doesn't untrim",
+ untrimmedValue: BrowserUIUtils.trimURLProtocol + "www.example.com/",
+ execute() {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ Assert.equal(gURLBar.selectionStart, 0, "Selection start is 0.");
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "Selection end is at and of text."
+ );
+ },
+ shouldUntrim: false,
+ },
+ {
+ description: "Test CTRL+L doesn't untrim",
+ untrimmedValue: BrowserUIUtils.trimURLProtocol + "www.example.com/",
+ execute() {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ Assert.equal(gURLBar.selectionStart, 0, "Selection start is 0.");
+ Assert.equal(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "Selection end is at and of text."
+ );
+ },
+ shouldUntrim: false,
+ },
+ {
+ description: "Test drag selection untrims",
+ untrimmedValue: BrowserUIUtils.trimURLProtocol + "www.example.com/",
+ execute() {
+ selectWithMouseDrag(100, 200);
+ Assert.greater(gURLBar.selectionStart, 0, "Selection start is positive.");
+ Assert.greater(
+ gURLBar.selectionEnd,
+ gURLBar.selectionStart,
+ "Selection is not empty."
+ );
+ },
+ shouldUntrim: true,
+ },
+ {
+ description: "Test double click selection untrims",
+ untrimmedValue: BrowserUIUtils.trimURLProtocol + "www.example.com/",
+ execute() {
+ selectWithDoubleClick(200);
+ Assert.greater(gURLBar.selectionStart, 0, "Selection start is positive.");
+ Assert.greater(
+ gURLBar.selectionEnd,
+ gURLBar.selectionStart,
+ "Selection is not empty."
+ );
+ },
+ shouldUntrim: true,
+ },
+ {
+ description: "Test click, LEFT untrims",
+ untrimmedValue: BrowserUIUtils.trimURLProtocol + "www.example.com/",
+ execute() {
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ },
+ shouldUntrim: true,
+ },
+ {
+ description: "Test CTRL+L, HOME untrims",
+ untrimmedValue: BrowserUIUtils.trimURLProtocol + "www.example.com/",
+ execute() {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ if (AppConstants.platform == "macosx") {
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true });
+ } else {
+ EventUtils.synthesizeKey("KEY_Home");
+ }
+ },
+ shouldUntrim: true,
+ },
+ {
+ description: "Test SHIFT+LEFT untrims",
+ untrimmedValue: BrowserUIUtils.trimURLProtocol + "www.example.com/",
+ async execute() {
+ EventUtils.synthesizeKey("l", { accelKey: true });
+ EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true });
+ Assert.equal(gURLBar.selectionStart, 0, "Selection start is 0.");
+ Assert.less(
+ gURLBar.selectionEnd,
+ gURLBar.value.length,
+ "Selection skips last characters."
+ );
+ },
+ shouldUntrim: true,
+ },
+];
+
+add_task(async function test_untrim() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.untrimOnUserInteraction.featureGate", true]],
+ });
+
+ for (let test of tests) {
+ info(test.description);
+ let trimmedValue = UrlbarTestUtils.trimURL(test.untrimmedValue);
+ gURLBar._setValue(test.untrimmedValue, {
+ allowTrim: true,
+ valueIsTyped: false,
+ });
+ gURLBar.blur();
+ Assert.equal(gURLBar.value, trimmedValue, "Value has been trimmed");
+ await test.execute();
+ Assert.equal(
+ gURLBar.value,
+ test.shouldUntrim ? test.untrimmedValue : trimmedValue,
+ "Value has been untrimmed"
+ );
+ gURLBar.handleRevert();
+ }
+});
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_autocomplete_edit_completed.js b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js
index 4fa60f6bf3..220634eb2c 100644
--- a/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js
+++ b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js
@@ -51,6 +51,7 @@ add_task(async function () {
info("Press backspace");
EventUtils.synthesizeKey("KEY_Backspace");
+ info("Backspaced value is " + gURLBar.value);
await UrlbarTestUtils.promiseSearchComplete(window);
let editedValue = gURLBar.value;
diff --git a/browser/components/urlbar/tests/browser/browser_contextualsearch.js b/browser/components/urlbar/tests/browser/browser_contextualsearch.js
index 60e489a542..449d1864c2 100644
--- a/browser/components/urlbar/tests/browser/browser_contextualsearch.js
+++ b/browser/components/urlbar/tests/browser/browser_contextualsearch.js
@@ -3,22 +3,54 @@
"use strict";
-const { UrlbarProviderContextualSearch } = ChromeUtils.importESModule(
- "resource:///modules/UrlbarProviderContextualSearch.sys.mjs"
+const { ActionsProviderContextualSearch } = ChromeUtils.importESModule(
+ "resource:///modules/ActionsProviderContextualSearch.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
);
add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
- set: [["browser.urlbar.contextualSearch.enabled", true]],
+ set: [
+ ["browser.urlbar.contextualSearch.enabled", true],
+ ["browser.urlbar.secondaryActions.featureGate", true],
+ ],
});
-});
-add_task(async function test_selectContextualSearchResult_already_installed() {
- await SearchTestUtils.installSearchExtension({
+ let ext = await SearchTestUtils.installSearchExtension({
name: "Contextual",
search_url: "https://example.com/browser",
});
+ await AddonTestUtils.waitForSearchProviderStartup(ext);
+});
+
+add_task(async function test_no_engine() {
+ const ENGINE_TEST_URL = "https://example.org/";
+ let onLoaded = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ ENGINE_TEST_URL
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ ENGINE_TEST_URL
+ );
+ await onLoaded;
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ });
+ Assert.ok(
+ UrlbarTestUtils.getResultCount(window) > 0,
+ "At least one result is shown"
+ );
+});
+
+add_task(async function test_selectContextualSearchResult_already_installed() {
const ENGINE_TEST_URL = "https://example.com/";
let onLoaded = BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
@@ -44,25 +76,15 @@ add_task(async function test_selectContextualSearchResult_already_installed() {
window,
value: query,
});
- const resultIndex = UrlbarTestUtils.getResultCount(window) - 1;
- const result = await UrlbarTestUtils.getDetailsOfResultAt(
- window,
- resultIndex
- );
-
- is(
- result.dynamicType,
- "contextualSearch",
- "Second last result is a contextual search result"
- );
info("Focus and select the contextual search result");
- UrlbarTestUtils.setSelectedRowIndex(window, resultIndex);
let onLoad = BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
false,
expectedUrl
);
+
+ EventUtils.synthesizeKey("KEY_Tab");
EventUtils.synthesizeKey("KEY_Enter");
await onLoad;
@@ -95,25 +117,14 @@ add_task(async function test_selectContextualSearchResult_not_installed() {
window,
value: query,
});
- const resultIndex = UrlbarTestUtils.getResultCount(window) - 1;
- const result = await UrlbarTestUtils.getDetailsOfResultAt(
- window,
- resultIndex
- );
-
- Assert.equal(
- result.dynamicType,
- "contextualSearch",
- "Second last result is a contextual search result"
- );
info("Focus and select the contextual search result");
- UrlbarTestUtils.setSelectedRowIndex(window, resultIndex);
let onLoad = BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
false,
EXPECTED_URL
);
+ EventUtils.synthesizeKey("KEY_Tab");
EventUtils.synthesizeKey("KEY_Enter");
await onLoad;
@@ -122,4 +133,6 @@ add_task(async function test_selectContextualSearchResult_not_installed() {
EXPECTED_URL,
"Selecting the contextual search result opens the search URL"
);
+
+ ActionsProviderContextualSearch.resetForTesting();
});
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_decode.js b/browser/components/urlbar/tests/browser/browser_decode.js
index 577d39b587..ee7831eea6 100644
--- a/browser/components/urlbar/tests/browser/browser_decode.js
+++ b/browser/components/urlbar/tests/browser/browser_decode.js
@@ -72,7 +72,7 @@ add_task(async function actionURILosslessDecode() {
Assert.equal(
gURLBar.value,
- UrlbarTestUtils.trimURL(urlNoScheme),
+ urlNoScheme,
"The string displayed in the textbox should not be escaped"
);
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_keepStateAcrossTabSwitches.js b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js
index 6ad6ce43e6..abe12846ab 100644
--- a/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js
+++ b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js
@@ -3,6 +3,10 @@
"use strict";
+add_setup(async function setup() {
+ registerCleanupFunction(PlacesUtils.history.clear);
+});
+
/**
* Verify user typed text remains in the URL bar when tab switching, even when
* loads fail.
@@ -78,94 +82,94 @@ add_task(async function invalidURL() {
* Test the urlbar status of text selection and focusing by tab switching.
*/
add_task(async function selectAndFocus() {
- // Create a tab with normal web page. Use a test-url that uses a protocol that
- // is not trimmed.
- const webpageTabURL =
- UrlbarTestUtils.getTrimmedProtocolWithSlashes() == "https://"
- ? "http://example.com"
- : "https://example.com";
- const webpageTab = await BrowserTestUtils.openNewForegroundTab({
- gBrowser,
- url: webpageTabURL,
- });
+ // Test both protocols to ensure we're testing any trimming case.
+ for (let protocol of ["http://", "https://"]) {
+ const webpageTabURL = protocol + "example.com";
+ const webpageTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: webpageTabURL,
+ });
- // Create a tab with userTypedValue.
- const userTypedTabText = "test";
- const userTypedTab = await BrowserTestUtils.openNewForegroundTab({
- gBrowser,
- });
- await UrlbarTestUtils.inputIntoURLBar(window, userTypedTabText);
+ // Create a tab with userTypedValue.
+ const userTypedTabText = "test";
+ const userTypedTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ });
+ await UrlbarTestUtils.inputIntoURLBar(window, userTypedTabText);
- // Create an empty tab.
- const emptyTab = await BrowserTestUtils.openNewForegroundTab({ gBrowser });
+ // Create an empty tab.
+ const emptyTab = await BrowserTestUtils.openNewForegroundTab({ gBrowser });
- registerCleanupFunction(async () => {
- await PlacesUtils.history.clear();
- BrowserTestUtils.removeTab(webpageTab);
- BrowserTestUtils.removeTab(userTypedTab);
- BrowserTestUtils.removeTab(emptyTab);
- });
+ async function cleanup() {
+ await PlacesUtils.history.clear();
+ BrowserTestUtils.removeTab(webpageTab);
+ BrowserTestUtils.removeTab(userTypedTab);
+ BrowserTestUtils.removeTab(emptyTab);
+ }
- await doSelectAndFocusTest({
- targetTab: webpageTab,
- targetSelectionStart: 0,
- targetSelectionEnd: 0,
- anotherTab: userTypedTab,
- });
- await doSelectAndFocusTest({
- targetTab: webpageTab,
- targetSelectionStart: 2,
- targetSelectionEnd: 5,
- anotherTab: userTypedTab,
- });
- await doSelectAndFocusTest({
- targetTab: webpageTab,
- targetSelectionStart: webpageTabURL.length,
- targetSelectionEnd: webpageTabURL.length,
- anotherTab: userTypedTab,
- });
- await doSelectAndFocusTest({
- targetTab: webpageTab,
- targetSelectionStart: 0,
- targetSelectionEnd: 0,
- anotherTab: emptyTab,
- });
- await doSelectAndFocusTest({
- targetTab: userTypedTab,
- targetSelectionStart: 0,
- targetSelectionEnd: 0,
- anotherTab: webpageTab,
- });
- await doSelectAndFocusTest({
- targetTab: userTypedTab,
- targetSelectionStart: 0,
- targetSelectionEnd: 0,
- anotherTab: emptyTab,
- });
- await doSelectAndFocusTest({
- targetTab: userTypedTab,
- targetSelectionStart: 1,
- targetSelectionEnd: 2,
- anotherTab: emptyTab,
- });
- await doSelectAndFocusTest({
- targetTab: userTypedTab,
- targetSelectionStart: userTypedTabText.length,
- targetSelectionEnd: userTypedTabText.length,
- anotherTab: emptyTab,
- });
- await doSelectAndFocusTest({
- targetTab: emptyTab,
- targetSelectionStart: 0,
- targetSelectionEnd: 0,
- anotherTab: webpageTab,
- });
- await doSelectAndFocusTest({
- targetTab: emptyTab,
- targetSelectionStart: 0,
- targetSelectionEnd: 0,
- anotherTab: userTypedTab,
- });
+ await doSelectAndFocusTest({
+ targetTab: webpageTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: userTypedTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: webpageTab,
+ targetSelectionStart: 2,
+ targetSelectionEnd: 5,
+ anotherTab: userTypedTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: webpageTab,
+ targetSelectionStart: webpageTabURL.length,
+ targetSelectionEnd: webpageTabURL.length,
+ anotherTab: userTypedTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: webpageTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: emptyTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: userTypedTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: webpageTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: userTypedTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: emptyTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: userTypedTab,
+ targetSelectionStart: 1,
+ targetSelectionEnd: 2,
+ anotherTab: emptyTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: userTypedTab,
+ targetSelectionStart: userTypedTabText.length,
+ targetSelectionEnd: userTypedTabText.length,
+ anotherTab: emptyTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: emptyTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: webpageTab,
+ });
+ await doSelectAndFocusTest({
+ targetTab: emptyTab,
+ targetSelectionStart: 0,
+ targetSelectionEnd: 0,
+ anotherTab: userTypedTab,
+ });
+
+ await cleanup();
+ }
});
async function doSelectAndFocusTest({
@@ -197,6 +201,13 @@ async function doSelectAndFocusTest({
targetSelectionStart,
targetSelectionEnd
);
+ const targetSelectedText = getSelectedText();
+ if (gURLBar.selectionStart != gURLBar.selectionEnd) {
+ Assert.ok(
+ targetSelectedText,
+ `Some text is selected: "${targetSelectedText}"`
+ );
+ }
const targetValue = gURLBar.value;
// Switch to another tab.
@@ -210,8 +221,9 @@ async function doSelectAndFocusTest({
Assert.equal(gURLBar.value, targetValue);
Assert.equal(gURLBar.focused, targetFocus);
if (gURLBar.focused) {
- Assert.equal(gURLBar.selectionStart, targetSelectionStart);
- Assert.equal(gURLBar.selectionEnd, targetSelectionEnd);
+ // Check the selected text rather than the selection indices, to keep
+ // untrimming into account.
+ Assert.equal(targetSelectedText, getSelectedText());
} else {
Assert.equal(gURLBar.selectionStart, gURLBar.value.length);
Assert.equal(gURLBar.selectionEnd, gURLBar.value.length);
@@ -221,12 +233,21 @@ async function doSelectAndFocusTest({
function setURLBarFocus(focus) {
if (focus) {
- gURLBar.focus();
+ // Simulate a user interaction, to eventually cause untrimming.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {});
} else {
gURLBar.blur();
}
}
+function getSelectedText() {
+ return gURLBar.inputField.editor.selection.toStringWithFormat(
+ "text/plain",
+ Ci.nsIDocumentEncoder.OutputPreformatted | Ci.nsIDocumentEncoder.OutputRaw,
+ 0
+ );
+}
+
async function switchTab(tab) {
if (gBrowser.selectedTab !== tab) {
EventUtils.synthesizeMouseAtCenter(tab, {});
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..44a7ea64b2
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_less_common_selection_manipulations.js
@@ -0,0 +1,240 @@
+/* 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;
+}
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_oneOffs.js b/browser/components/urlbar/tests/browser/browser_oneOffs.js
index 0c04f1e321..e517ea0a9a 100644
--- a/browser/components/urlbar/tests/browser/browser_oneOffs.js
+++ b/browser/components/urlbar/tests/browser/browser_oneOffs.js
@@ -30,7 +30,6 @@ add_setup(async function () {
set: [
["browser.search.separatePrivateDefault.ui.enabled", false],
["browser.urlbar.suggest.quickactions", false],
- ["browser.urlbar.shortcuts.quickactions", true],
],
});
@@ -943,7 +942,7 @@ async function doLocalShortcutsShownTest() {
await rebuildPromise;
let buttons = oneOffSearchButtons.localButtons;
- Assert.equal(buttons.length, 4, "Expected number of local shortcuts");
+ Assert.equal(buttons.length, 3, "Expected number of local shortcuts");
let expectedSource;
let seenIDs = new Set();
@@ -963,9 +962,6 @@ async function doLocalShortcutsShownTest() {
case "urlbar-engine-one-off-item-history":
expectedSource = UrlbarUtils.RESULT_SOURCE.HISTORY;
break;
- case "urlbar-engine-one-off-item-actions":
- expectedSource = UrlbarUtils.RESULT_SOURCE.ACTIONS;
- break;
default:
Assert.ok(false, `Unexpected local shortcut ID: ${button.id}`);
break;
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_quickactions.js b/browser/components/urlbar/tests/browser/browser_quickactions.js
index ccf045d9e8..1fc4ef7cd6 100644
--- a/browser/components/urlbar/tests/browser/browser_quickactions.js
+++ b/browser/components/urlbar/tests/browser/browser_quickactions.js
@@ -10,32 +10,41 @@
ChromeUtils.defineESModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
UpdateService: "resource://gre/modules/UpdateService.sys.mjs",
- UrlbarProviderQuickActions:
- "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+ ActionsProviderQuickActions:
+ "resource:///modules/ActionsProviderQuickActions.sys.mjs",
});
const DUMMY_PAGE =
- "http://example.com/browser/browser/base/content/test/general/dummy_page.html";
+ "https://example.com/browser/browser/base/content/test/general/dummy_page.html";
let testActionCalled = 0;
+const assertAction = async name => {
+ await BrowserTestUtils.waitForCondition(() =>
+ window.document.querySelector(`.urlbarView-action-btn[data-action=${name}]`)
+ );
+ Assert.ok(true, `We found action "${name}`);
+};
+
+const hasQuickActions = win =>
+ !!win.document.querySelector(".urlbarView-action-btn");
+
add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.urlbar.quickactions.enabled", true],
- ["browser.urlbar.suggest.quickactions", true],
- ["browser.urlbar.shortcuts.quickactions", true],
+ ["browser.urlbar.secondaryActions.featureGate", true],
],
});
- UrlbarProviderQuickActions.addAction("testaction", {
+ ActionsProviderQuickActions.addAction("testaction", {
commands: ["testaction"],
label: "quickactions-downloads2",
onPick: () => testActionCalled++,
});
registerCleanupFunction(() => {
- UrlbarProviderQuickActions.removeAction("testaction");
+ ActionsProviderQuickActions.removeAction("testaction");
});
});
@@ -57,183 +66,16 @@ add_task(async function basic() {
value: "testact",
});
- Assert.equal(
- UrlbarTestUtils.getResultCount(window),
- 2,
- "We matched the action"
- );
+ await assertAction("testaction");
info("The callback of the action is fired when selected");
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
- Assert.equal(testActionCalled, 1, "Test actionwas called");
-});
-
-add_task(async function test_label_command() {
- info("A prefix of the label matches");
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "View Dow",
- });
- Assert.equal(
- UrlbarTestUtils.getResultCount(window),
- 2,
- "We matched the action"
- );
-
- let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
- Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
- Assert.equal(result.providerName, "quickactions");
- await UrlbarTestUtils.promisePopupClose(window, () => {
- EventUtils.synthesizeKey("KEY_Escape");
- });
-});
-
-add_task(async function enter_search_mode_button() {
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "test",
- });
-
- await clickQuickActionOneoffButton();
-
- await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
- Assert.ok(true, "Actions are shown when we enter actions search mode.");
-
- await UrlbarTestUtils.exitSearchMode(window);
- await UrlbarTestUtils.promisePopupClose(window);
- EventUtils.synthesizeKey("KEY_Escape");
-});
-
-add_task(async function enter_search_mode_oneoff_by_key() {
- // Select actions oneoff button by keyboard.
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "",
- });
- await UrlbarTestUtils.enterSearchMode(window);
- const oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window);
- for (;;) {
- EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
- if (
- oneOffButtons.selectedButton.source === UrlbarUtils.RESULT_SOURCE.ACTIONS
- ) {
- break;
- }
- }
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: " ",
- });
- await UrlbarTestUtils.assertSearchMode(window, {
- source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
- entry: "oneoff",
- });
-
- await UrlbarTestUtils.exitSearchMode(window);
- await UrlbarTestUtils.promisePopupClose(window);
- EventUtils.synthesizeKey("KEY_Escape");
-});
-
-add_task(async function enter_search_mode_key() {
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "> ",
- });
- await UrlbarTestUtils.assertSearchMode(window, {
- source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
- entry: "typed",
- });
- Assert.equal(
- await hasQuickActions(window),
- true,
- "Actions are shown in search mode"
- );
- await UrlbarTestUtils.exitSearchMode(window);
- await UrlbarTestUtils.promisePopupClose(window);
- EventUtils.synthesizeKey("KEY_Escape");
-});
-
-add_task(async function test_disabled() {
- UrlbarProviderQuickActions.addAction("disabledaction", {
- commands: ["disabledaction"],
- isActive: () => false,
- label: "quickactions-restart",
- });
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "disabled",
- });
-
- Assert.equal(
- await hasQuickActions(window),
- false,
- "Result for quick actions is hidden"
- );
-
- await UrlbarTestUtils.promisePopupClose(window);
- UrlbarProviderQuickActions.removeAction("disabledaction");
-});
-
-/**
- * The first part of this test confirms that when the screenshots component is enabled
- * the screenshot quick action button will be enabled on about: pages.
- * The second part confirms that when the screenshots extension is enabled the
- * screenshot quick action button will be disbaled on about: pages.
- */
-add_task(async function test_screenshot_enabled_or_disabled() {
- let onLoaded = BrowserTestUtils.browserLoaded(
- gBrowser.selectedBrowser,
- false,
- "about:blank"
- );
- BrowserTestUtils.startLoadingURIString(
- gBrowser.selectedBrowser,
- "about:blank"
- );
- await onLoaded;
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "screenshot",
- });
- Assert.equal(
- UrlbarTestUtils.getResultCount(window),
- 2,
- "The action is displayed"
- );
- let screenshotButton = window.document.querySelector(
- ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button"
- );
- Assert.ok(
- !screenshotButton.hasAttribute("disabled"),
- "Screenshot button is enabled on about pages"
- );
-
- await UrlbarTestUtils.promisePopupClose(window);
- EventUtils.synthesizeKey("KEY_Escape");
-
- await SpecialPowers.pushPrefEnv({
- set: [["screenshots.browser.component.enabled", false]],
- });
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "screenshot",
- });
- Assert.equal(
- await hasQuickActions(window),
- false,
- "Result for quick actions is hidden"
- );
-
- await UrlbarTestUtils.promisePopupClose(window);
+ Assert.equal(testActionCalled, 1, "Test action was called");
});
add_task(async function match_in_phrase() {
- UrlbarProviderQuickActions.addAction("newtestaction", {
+ ActionsProviderQuickActions.addAction("newtestaction", {
commands: ["matchingstring"],
label: "quickactions-downloads2",
});
@@ -243,304 +85,31 @@ add_task(async function match_in_phrase() {
window,
value: "Test we match at end of matchingstring",
});
- Assert.equal(
- UrlbarTestUtils.getResultCount(window),
- 2,
- "We matched the action"
- );
- await UrlbarTestUtils.promisePopupClose(window);
- EventUtils.synthesizeKey("KEY_Escape");
- UrlbarProviderQuickActions.removeAction("newtestaction");
-});
-
-add_task(async function test_other_search_mode() {
- let defaultEngine = await SearchTestUtils.promiseNewSearchEngine({
- url: getRootDirectory(gTestPath) + "searchSuggestionEngine.xml",
- });
- defaultEngine.alias = "testalias";
- let oldDefaultEngine = await Services.search.getDefault();
- Services.search.setDefault(
- defaultEngine,
- Ci.nsISearchService.CHANGE_REASON_UNKNOWN
- );
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: defaultEngine.alias + " ",
- });
- Assert.equal(
- UrlbarTestUtils.getResultCount(window),
- 0,
- "The results should be empty as no actions are displayed in other search modes"
- );
- await UrlbarTestUtils.assertSearchMode(window, {
- engineName: defaultEngine.name,
- entry: "typed",
- });
- await UrlbarTestUtils.promisePopupClose(window, () => {
- EventUtils.synthesizeKey("KEY_Escape");
- });
- Services.search.setDefault(
- oldDefaultEngine,
- Ci.nsISearchService.CHANGE_REASON_UNKNOWN
- );
-});
-
-add_task(async function test_no_quickactions_suggestions() {
- await SpecialPowers.pushPrefEnv({
- set: [
- ["browser.urlbar.suggest.quickactions", false],
- ["screenshots.browser.component.enabled", true],
- ],
- });
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "screenshot",
- });
- Assert.ok(
- !window.document.querySelector(
- ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button"
- ),
- "Screenshot button is not suggested"
- );
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "> screenshot",
- });
- Assert.ok(
- window.document.querySelector(
- ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button"
- ),
- "Screenshot button is suggested"
- );
-
- await UrlbarTestUtils.promisePopupClose(window);
- EventUtils.synthesizeKey("KEY_Escape");
-
- await SpecialPowers.popPrefEnv();
-});
-
-add_task(async function test_quickactions_disabled() {
- await SpecialPowers.pushPrefEnv({
- set: [
- ["browser.urlbar.quickactions.enabled", false],
- ["browser.urlbar.suggest.quickactions", true],
- ],
- });
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "screenshot",
- });
-
- Assert.ok(
- !window.document.querySelector(
- ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button"
- ),
- "Screenshot button is not suggested"
- );
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "> screenshot",
- });
- Assert.ok(
- !window.document.querySelector(
- ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button"
- ),
- "Screenshot button is not suggested"
- );
-
+ await assertAction("newtestaction");
await UrlbarTestUtils.promisePopupClose(window);
EventUtils.synthesizeKey("KEY_Escape");
-
- await SpecialPowers.popPrefEnv();
-});
-
-let COMMANDS_TESTS = [
- {
- cmd: "add-ons",
- uri: "about:addons",
- testFun: async () => isSelected("button[name=discover]"),
- },
- {
- cmd: "plugins",
- uri: "about:addons",
- testFun: async () => isSelected("button[name=plugin]"),
- },
- {
- cmd: "extensions",
- uri: "about:addons",
- testFun: async () => isSelected("button[name=extension]"),
- },
- {
- cmd: "themes",
- uri: "about:addons",
- testFun: async () => isSelected("button[name=theme]"),
- },
- {
- cmd: "add-ons",
- setup: async () => {
- const onLoad = BrowserTestUtils.browserLoaded(
- gBrowser.selectedBrowser,
- false,
- "http://example.com/"
- );
- BrowserTestUtils.startLoadingURIString(
- gBrowser.selectedBrowser,
- "http://example.com/"
- );
- await onLoad;
- },
- uri: "about:addons",
- isNewTab: true,
- testFun: async () => isSelected("button[name=discover]"),
- },
- {
- cmd: "plugins",
- setup: async () => {
- const onLoad = BrowserTestUtils.browserLoaded(
- gBrowser.selectedBrowser,
- false,
- "http://example.com/"
- );
- BrowserTestUtils.startLoadingURIString(
- gBrowser.selectedBrowser,
- "http://example.com/"
- );
- await onLoad;
- },
- uri: "about:addons",
- isNewTab: true,
- testFun: async () => isSelected("button[name=plugin]"),
- },
- {
- cmd: "extensions",
- setup: async () => {
- const onLoad = BrowserTestUtils.browserLoaded(
- gBrowser.selectedBrowser,
- false,
- "http://example.com/"
- );
- BrowserTestUtils.startLoadingURIString(
- gBrowser.selectedBrowser,
- "http://example.com/"
- );
- await onLoad;
- },
- uri: "about:addons",
- isNewTab: true,
- testFun: async () => isSelected("button[name=extension]"),
- },
- {
- cmd: "themes",
- setup: async () => {
- const onLoad = BrowserTestUtils.browserLoaded(
- gBrowser.selectedBrowser,
- false,
- "http://example.com/"
- );
- BrowserTestUtils.startLoadingURIString(
- gBrowser.selectedBrowser,
- "http://example.com/"
- );
- await onLoad;
- },
- uri: "about:addons",
- isNewTab: true,
- testFun: async () => isSelected("button[name=theme]"),
- },
-];
-
-let isSelected = async selector =>
- SpecialPowers.spawn(gBrowser.selectedBrowser, [selector], arg => {
- return ContentTaskUtils.waitForCondition(() =>
- content.document.querySelector(arg)?.hasAttribute("selected")
- );
- });
-
-add_task(async function test_pages() {
- for (const { cmd, uri, setup, isNewTab, testFun } of COMMANDS_TESTS) {
- info(`Testing ${cmd} command is triggered`);
- let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
-
- if (setup) {
- info("Setup");
- await setup();
- }
-
- let onLoad = isNewTab
- ? BrowserTestUtils.waitForNewTab(gBrowser, uri, true)
- : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri);
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: cmd,
- });
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
- EventUtils.synthesizeKey("KEY_Enter", {}, window);
-
- const newTab = await onLoad;
-
- Assert.ok(
- await testFun(),
- `The command "${cmd}" passed completed its test`
- );
-
- if (isNewTab) {
- await BrowserTestUtils.removeTab(newTab);
- }
- await BrowserTestUtils.removeTab(tab);
- }
+ ActionsProviderQuickActions.removeAction("newtestaction");
});
-const assertActionButtonStatus = async (name, expectedEnabled, description) => {
- await BrowserTestUtils.waitForCondition(() =>
- window.document.querySelector(`[data-key=${name}]`)
- );
- const target = window.document.querySelector(`[data-key=${name}]`);
- Assert.equal(!target.hasAttribute("disabled"), expectedEnabled, description);
-};
-
add_task(async function test_viewsource() {
info("Check the button status of when the page is not web content");
const tab = await BrowserTestUtils.openNewForegroundTab({
gBrowser,
- opening: "about:home",
+ opening: "https://example.com",
waitForLoad: true,
});
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "viewsource",
- });
- await assertActionButtonStatus(
- "viewsource",
- true,
- "Should be enabled even if the page is not web content"
- );
- info("Check the button status of when the page is web content");
- BrowserTestUtils.startLoadingURIString(
- gBrowser.selectedBrowser,
- "http://example.com"
- );
- await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "viewsource",
});
- await assertActionButtonStatus(
- "viewsource",
- true,
- "Should be enabled on web content as well"
- );
info("Do view source action");
const onLoad = BrowserTestUtils.waitForNewTab(
gBrowser,
- "view-source:http://example.com/"
+ "view-source:https://example.com/"
);
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
const viewSourceTab = await onLoad;
@@ -551,7 +120,7 @@ add_task(async function test_viewsource() {
});
Assert.equal(
- await hasQuickActions(window),
+ hasQuickActions(window),
false,
"Result for quick actions is hidden"
);
@@ -575,7 +144,7 @@ async function doAlertDialogTest({ input, dialogContentURI }) {
},
});
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
await onDialog;
@@ -601,16 +170,16 @@ add_task(async function test_clear() {
});
});
-async function doUpdateActionTest(isActiveExpected, description) {
+async function doUpdateActionTest(isActiveExpected) {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "update",
});
if (isActiveExpected) {
- await assertActionButtonStatus("update", isActiveExpected, description);
+ await assertAction("update");
} else {
- Assert.equal(await hasQuickActions(window), false, description);
+ Assert.equal(hasQuickActions(window), false, "No QuickActions were shown");
}
}
@@ -644,43 +213,6 @@ add_task(async function test_update() {
}
});
-async function hasQuickActions(win) {
- for (let i = 0, count = UrlbarTestUtils.getResultCount(win); i < count; i++) {
- const { result } = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
- if (result.providerName === "quickactions") {
- return true;
- }
- }
- return false;
-}
-
-add_task(async function test_show_in_zero_prefix() {
- for (const minimumSearchString of [0, 3]) {
- info(
- `Test when quickactions.minimumSearchString pref is ${minimumSearchString}`
- );
- await SpecialPowers.pushPrefEnv({
- set: [
- [
- "browser.urlbar.quickactions.minimumSearchString",
- minimumSearchString,
- ],
- ],
- });
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "",
- });
-
- Assert.equal(
- await hasQuickActions(window),
- !minimumSearchString,
- "Result for quick actions is as expected"
- );
- await SpecialPowers.popPrefEnv();
- }
-});
-
add_task(async function test_whitespace() {
info("Test with quickactions.showInZeroPrefix pref is false");
await SpecialPowers.pushPrefEnv({
@@ -691,7 +223,7 @@ add_task(async function test_whitespace() {
value: " ",
});
Assert.equal(
- await hasQuickActions(window),
+ hasQuickActions(window),
false,
"Result for quick actions is not shown"
);
diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_commands.js b/browser/components/urlbar/tests/browser/browser_quickactions_commands.js
new file mode 100644
index 0000000000..19b8d31ada
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_quickactions_commands.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test QuickActions.
+ */
+
+"use strict";
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quickactions.enabled", true],
+ ["browser.urlbar.secondaryActions.featureGate", true],
+ ],
+ });
+});
+
+let COMMANDS_TESTS = [
+ {
+ cmd: "add-ons",
+ uri: "about:addons",
+ testFun: async () => isSelected("button[name=discover]"),
+ },
+ {
+ cmd: "plugins",
+ uri: "about:addons",
+ testFun: async () => isSelected("button[name=plugin]"),
+ },
+ {
+ cmd: "extensions",
+ uri: "about:addons",
+ testFun: async () => isSelected("button[name=extension]"),
+ },
+ {
+ cmd: "themes",
+ uri: "about:addons",
+ testFun: async () => isSelected("button[name=theme]"),
+ },
+ {
+ cmd: "add-ons",
+ setup: async () => {
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "https://example.com/"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "https://example.com/"
+ );
+ await onLoad;
+ },
+ uri: "about:addons",
+ isNewTab: true,
+ testFun: async () => isSelected("button[name=discover]"),
+ },
+ {
+ cmd: "plugins",
+ setup: async () => {
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "https://example.com/"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "https://example.com/"
+ );
+ await onLoad;
+ },
+ uri: "about:addons",
+ isNewTab: true,
+ testFun: async () => isSelected("button[name=plugin]"),
+ },
+ {
+ cmd: "extensions",
+ setup: async () => {
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "https://example.com/"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "https://example.com/"
+ );
+ await onLoad;
+ },
+ uri: "about:addons",
+ isNewTab: true,
+ testFun: async () => isSelected("button[name=extension]"),
+ },
+ {
+ cmd: "themes",
+ setup: async () => {
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "https://example.com/"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "https://example.com/"
+ );
+ await onLoad;
+ },
+ uri: "about:addons",
+ isNewTab: true,
+ testFun: async () => isSelected("button[name=theme]"),
+ },
+];
+
+let isSelected = async selector =>
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [selector], arg => {
+ return ContentTaskUtils.waitForCondition(() =>
+ content.document.querySelector(arg)?.hasAttribute("selected")
+ );
+ });
+
+add_task(async function test_pages() {
+ for (const { cmd, uri, setup, isNewTab, testFun } of COMMANDS_TESTS) {
+ info(`Testing ${cmd} command is triggered`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ if (setup) {
+ info("Setup");
+ await setup();
+ }
+
+ let onLoad = isNewTab
+ ? BrowserTestUtils.waitForNewTab(gBrowser, uri, true)
+ : BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: cmd,
+ });
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+
+ const newTab = await onLoad;
+
+ Assert.ok(
+ await testFun(),
+ `The command "${cmd}" passed completed its test`
+ );
+
+ if (isNewTab) {
+ await BrowserTestUtils.removeTab(newTab);
+ }
+ await BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js b/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js
index 1e1e92fb31..bee98d42af 100644
--- a/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js
+++ b/browser/components/urlbar/tests/browser/browser_quickactions_devtools.js
@@ -17,7 +17,7 @@ add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.urlbar.quickactions.enabled", true],
- ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.secondaryActions.featureGate", true],
["browser.urlbar.shortcuts.quickactions", true],
],
});
@@ -25,21 +25,14 @@ add_setup(async function setup() {
const assertActionButtonStatus = async (name, expectedEnabled, description) => {
await BrowserTestUtils.waitForCondition(() =>
- window.document.querySelector(`[data-key=${name}]`)
+ window.document.querySelector(`[data-action=${name}]`)
);
- const target = window.document.querySelector(`[data-key=${name}]`);
+ const target = window.document.querySelector(`[data-action=${name}]`);
Assert.equal(!target.hasAttribute("disabled"), expectedEnabled, description);
};
-async function hasQuickActions(win) {
- for (let i = 0, count = UrlbarTestUtils.getResultCount(win); i < count; i++) {
- const { result } = await UrlbarTestUtils.getDetailsOfResultAt(win, i);
- if (result.providerName === "quickactions") {
- return true;
- }
- }
- return false;
-}
+const hasQuickActions = win =>
+ !!win.document.querySelector(".urlbarView-action-btn");
add_task(async function test_inspector() {
const testData = [
@@ -129,7 +122,7 @@ add_task(async function test_inspector() {
);
} else {
Assert.equal(
- await hasQuickActions(window),
+ hasQuickActions(window),
false,
"Result for quick actions is not shown since the inspector tool is disabled"
);
@@ -142,7 +135,7 @@ add_task(async function test_inspector() {
}
info("Do inspect action");
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
await BrowserTestUtils.waitForCondition(
() => DevToolsShim.hasToolboxForTab(gBrowser.selectedTab),
@@ -160,7 +153,7 @@ add_task(async function test_inspector() {
value: "inspector",
});
Assert.equal(
- await hasQuickActions(window),
+ hasQuickActions(window),
false,
"Result for quick actions is not shown since the inspector is already opening"
);
diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js b/browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js
index c81442f0f5..c83fdff441 100644
--- a/browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js
+++ b/browser/components/urlbar/tests/browser/browser_quickactions_screenshot.js
@@ -34,11 +34,18 @@ async function clickQuickActionOneoffButton() {
});
}
+const assertAction = async name => {
+ await BrowserTestUtils.waitForCondition(() =>
+ window.document.querySelector(`.urlbarView-action-btn[data-action=${name}]`)
+ );
+ Assert.ok(true, `We found action "${name}`);
+};
+
add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.urlbar.quickactions.enabled", true],
- ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.secondaryActions.featureGate", true],
["browser.urlbar.shortcuts.quickactions", true],
],
});
@@ -61,17 +68,10 @@ add_task(async function test_screenshot() {
window,
value: "screenshot",
});
- Assert.equal(
- UrlbarTestUtils.getResultCount(window),
- 2,
- "We matched the action"
- );
- let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
- Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
- Assert.equal(result.providerName, "quickactions");
+ await assertAction("screenshot");
info("Trigger the screenshot mode");
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
await TestUtils.waitForCondition(
isScreenshotInitialized,
@@ -104,38 +104,6 @@ add_task(async function search_mode_on_webpage() {
});
await UrlbarTestUtils.promiseSearchComplete(window);
- info("Enter quick action search mode");
- await clickQuickActionOneoffButton();
- await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
- Assert.ok(true, "Actions are shown when we enter actions search mode.");
-
- info("Trigger the screenshot mode");
- const initialActionButtons = window.document.querySelectorAll(
- ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button"
- );
- let screenshotButton;
- for (let i = 0; i < initialActionButtons.length; i++) {
- const item = initialActionButtons.item(i);
- if (item.dataset.key === "screenshot") {
- screenshotButton = item;
- break;
- }
- }
- EventUtils.synthesizeMouseAtCenter(screenshotButton, {}, window);
- await TestUtils.waitForCondition(
- isScreenshotInitialized,
- "Screenshot component is active",
- 200,
- 100
- );
-
- info("Press Escape to exit screenshot mode");
- EventUtils.synthesizeKey("KEY_Escape", {}, window);
- await TestUtils.waitForCondition(
- async () => !(await isScreenshotInitialized()),
- "Screenshot component has been dismissed"
- );
-
info("Check the urlbar state");
Assert.equal(gURLBar.value, UrlbarTestUtils.trimURL("https://example.com"));
Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid");
@@ -146,23 +114,7 @@ add_task(async function search_mode_on_webpage() {
});
await UrlbarTestUtils.promiseSearchComplete(window);
- info("Enter quick action search mode again");
- await clickQuickActionOneoffButton();
- await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
- const finalActionButtons = window.document.querySelectorAll(
- ".urlbarView-row[dynamicType=quickactions] .urlbarView-quickaction-button"
- );
-
- info("Check the action buttons and the urlbar");
- Assert.equal(
- finalActionButtons.length,
- initialActionButtons.length,
- "The same buttons as initially displayed will display"
- );
- Assert.equal(gURLBar.value, "");
-
info("Clean up");
- await UrlbarTestUtils.exitSearchMode(window);
await UrlbarTestUtils.promisePopupClose(window);
EventUtils.synthesizeKey("KEY_Escape");
BrowserTestUtils.removeTab(tab);
diff --git a/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js b/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js
index abac861931..ee1f3cab62 100644
--- a/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js
+++ b/browser/components/urlbar/tests/browser/browser_quickactions_tab_refocus.js
@@ -13,7 +13,7 @@ add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.urlbar.quickactions.enabled", true],
- ["browser.urlbar.suggest.quickactions", true],
+ ["browser.urlbar.secondaryActions.featureGate", true],
["browser.urlbar.shortcuts.quickactions", true],
],
});
@@ -90,7 +90,7 @@ add_task(async function test_about_pages() {
window,
value: firstInput,
});
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
}
await onLoad;
@@ -106,7 +106,7 @@ add_task(async function test_about_pages() {
window,
value: secondInput || firstInput,
});
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
Assert.equal(
gBrowser.selectedTab,
@@ -158,7 +158,7 @@ add_task(async function test_about_addons_pages() {
window,
value: cmd,
});
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
Assert.ok(await testFun(), "The page content is correct");
@@ -175,7 +175,7 @@ add_task(async function test_about_addons_pages() {
window,
value: cmd,
});
- EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
await BrowserTestUtils.waitForCondition(() => testFun());
Assert.ok(true, "The tab correspondent action is selected");
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_secondaryActions.js b/browser/components/urlbar/tests/browser/browser_secondaryActions.js
new file mode 100644
index 0000000000..7e03ae036c
--- /dev/null
+++ b/browser/components/urlbar/tests/browser/browser_secondaryActions.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Basic tests for secondary Actions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ActionsProviderQuickActions:
+ "resource:///modules/ActionsProviderQuickActions.sys.mjs",
+});
+
+let testActionCalled = 0;
+
+let loadURI = async (browser, uri) => {
+ let onLoaded = BrowserTestUtils.browserLoaded(browser, false, uri);
+ BrowserTestUtils.startLoadingURIString(browser, uri);
+ return onLoaded;
+};
+
+add_setup(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quickactions.enabled", true],
+ ["browser.urlbar.secondaryActions.featureGate", true],
+ ["browser.urlbar.contextualSearch.enabled", true],
+ ],
+ });
+
+ ActionsProviderQuickActions.addAction("testaction", {
+ commands: ["testaction"],
+ actionKey: "testaction",
+ label: "quickactions-downloads2",
+ onPick: () => testActionCalled++,
+ });
+
+ registerCleanupFunction(() => {
+ ActionsProviderQuickActions.removeAction("testaction");
+ });
+});
+
+add_task(async function test_quickaction() {
+ info("Match an installed quickaction and trigger it via tab");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "testact",
+ });
+
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "We matched the action"
+ );
+
+ info("The callback of the action is fired when selected");
+ EventUtils.synthesizeKey("KEY_Tab", {}, window);
+ EventUtils.synthesizeKey("KEY_Enter", {}, window);
+ Assert.equal(testActionCalled, 1, "Test action was called");
+});
+
+add_task(async function test_switchtab() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await loadURI(win.gBrowser, "https://example.com/");
+
+ info("Open a new tab, type in the urlbar and switch to previous tab");
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "example",
+ });
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("KEY_Tab", {}, win);
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+
+ is(win.gBrowser.tabs.length, 1, "We switched to previous tab");
+ is(
+ win.gBrowser.currentURI.spec,
+ "https://example.com/",
+ "We switched to previous tab"
+ );
+
+ info(
+ "Open a new tab, type in the urlbar, select result and open url in current tab"
+ );
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser);
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window: win,
+ value: "example",
+ });
+
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ await BrowserTestUtils.browserLoaded(win.gBrowser);
+ is(win.gBrowser.tabs.length, 2, "We switched to previous tab");
+ is(
+ win.gBrowser.currentURI.spec,
+ "https://example.com/",
+ "We opened in current tab"
+ );
+
+ BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_sitesearch() {
+ await SearchTestUtils.installSearchExtension({
+ name: "Contextual",
+ search_url: "https://example.com/browser",
+ });
+
+ let ENGINE_TEST_URL = "https://example.com/";
+ await loadURI(gBrowser.selectedBrowser, ENGINE_TEST_URL);
+
+ const query = "search";
+ let engine = Services.search.getEngineByName("Contextual");
+ const [expectedUrl] = UrlbarUtils.getSearchQueryUrl(engine, query);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "sea",
+ });
+
+ let onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ expectedUrl
+ );
+ gURLBar.value = query;
+ UrlbarTestUtils.fireInputEvent(window);
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onLoad;
+
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ expectedUrl,
+ "Selecting the contextual search result opens the search URL"
+ );
+});
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_strip_on_share.js b/browser/components/urlbar/tests/browser/browser_strip_on_share.js
index 9e045cee9c..ec3f96425a 100644
--- a/browser/components/urlbar/tests/browser/browser_strip_on_share.js
+++ b/browser/components/urlbar/tests/browser/browser_strip_on_share.js
@@ -101,6 +101,32 @@ add_task(async function testQueryParamIsNotStrippedForWrongSiteSpecific() {
});
});
+// Ensuring site specific parameters are stripped regardless
+// of capitalization in the URI
+add_task(async function testQueryParamIsStrippedWhenParamIsCapitalized() {
+ let originalUrl = "https://www.example.com/?TEST_1=1234";
+ let shortenedUrl = "https://www.example.com/";
+ await testMenuItemEnabled({
+ selectWholeUrl: true,
+ validUrl: originalUrl,
+ strippedUrl: shortenedUrl,
+ useTestList: true,
+ });
+});
+
+// Ensuring site specific parameters are stripped regardless
+// of capitalization in the URI
+add_task(async function testQueryParamIsStrippedWhenParamIsCapitalized() {
+ let originalUrl = "https://www.example.com/?test_5=1234";
+ let shortenedUrl = "https://www.example.com/";
+ await testMenuItemEnabled({
+ selectWholeUrl: true,
+ validUrl: originalUrl,
+ strippedUrl: shortenedUrl,
+ useTestList: true,
+ });
+});
+
/**
* Opens a new tab, opens the ulr bar context menu and checks that the strip-on-share menu item is not visible
*
@@ -163,7 +189,7 @@ async function testMenuItemEnabled({
topLevelSites: ["*"],
},
example: {
- queryParams: ["test_2", "test_1"],
+ queryParams: ["test_2", "test_1", "TEST_5"],
topLevelSites: ["www.example.com"],
},
exampleNet: {
diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js
deleted file mode 100644
index b29807900b..0000000000
--- a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_quickactions.js
+++ /dev/null
@@ -1,133 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-/**
- * This file tests urlbar telemetry for quickactions.
- */
-
-"use strict";
-
-ChromeUtils.defineESModuleGetters(this, {
- UrlbarProviderQuickActions:
- "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
-});
-
-let testActionCalled = 0;
-
-add_setup(async function setup() {
- await SpecialPowers.pushPrefEnv({
- set: [
- ["browser.urlbar.suggest.quickactions", true],
- ["browser.urlbar.quickactions.enabled", true],
- ],
- });
-
- UrlbarProviderQuickActions.addAction("testaction", {
- commands: ["testaction"],
- label: "quickactions-downloads2",
- onPick: () => testActionCalled++,
- });
-
- registerCleanupFunction(() => {
- UrlbarProviderQuickActions.removeAction("testaction");
- });
-});
-
-add_task(async function test() {
- const histograms = snapshotHistograms();
-
- // Do a search to show the quickaction.
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "testaction",
- waitForFocus,
- fireInputEvent: true,
- });
-
- await UrlbarTestUtils.promisePopupClose(window, () => {
- EventUtils.synthesizeKey("KEY_ArrowDown");
- EventUtils.synthesizeKey("KEY_Enter");
- });
-
- Assert.equal(testActionCalled, 1, "Test action was called");
-
- TelemetryTestUtils.assertHistogram(
- histograms.resultMethodHist,
- UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection,
- 1
- );
-
- let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
- TelemetryTestUtils.assertKeyedScalar(
- scalars,
- `urlbar.picked.quickaction`,
- 1,
- 1
- );
-
- TelemetryTestUtils.assertKeyedScalar(
- scalars,
- "quickaction.picked",
- "testaction-10",
- 1
- );
-
- TelemetryTestUtils.assertKeyedScalar(
- scalars,
- "quickaction.impression",
- "testaction-10",
- 1
- );
-
- // Clean up for subsequent tests.
- gURLBar.handleRevert();
-});
-
-add_task(async function test_impressions() {
- UrlbarProviderQuickActions.addAction("testaction2", {
- commands: ["testaction2"],
- label: "quickactions-downloads2",
- onPick: () => {},
- });
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: "testaction",
- waitForFocus,
- fireInputEvent: true,
- });
-
- await UrlbarTestUtils.promisePopupClose(window, () => {
- EventUtils.synthesizeKey("KEY_ArrowDown");
- EventUtils.synthesizeKey("KEY_Enter");
- });
-
- let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
-
- TelemetryTestUtils.assertKeyedScalar(
- scalars,
- "quickaction.impression",
- `testaction-10`,
- 1
- );
- TelemetryTestUtils.assertKeyedScalar(
- scalars,
- "quickaction.impression",
- `testaction2-10`,
- 1
- );
-
- UrlbarProviderQuickActions.removeAction("testaction2");
- gURLBar.handleRevert();
-});
-
-function snapshotHistograms() {
- Services.telemetry.clearScalars();
- Services.telemetry.clearEvents();
- return {
- resultMethodHist: TelemetryTestUtils.getAndClearHistogram(
- "FX_URLBAR_SELECTED_RESULT_METHOD"
- ),
- };
-}
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/browser/browser_view_selectionByMouse.js b/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js
index 10110a8928..0625a3397f 100644
--- a/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js
+++ b/browser/components/urlbar/tests/browser/browser_view_selectionByMouse.js
@@ -6,16 +6,16 @@
// Test selection on result view by mouse.
ChromeUtils.defineESModuleGetters(this, {
- UrlbarProviderQuickActions:
- "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+ ActionsProviderQuickActions:
+ "resource:///modules/ActionsProviderQuickActions.sys.mjs",
});
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [
+ ["browser.urlbar.secondaryActions.featureGate", true],
["browser.urlbar.quickactions.enabled", true],
["browser.urlbar.suggest.quickactions", true],
- ["browser.urlbar.shortcuts.quickactions", true],
],
});
@@ -23,7 +23,7 @@ add_setup(async function () {
await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
- UrlbarProviderQuickActions.addAction("test-addons", {
+ ActionsProviderQuickActions.addAction("test-addons", {
commands: ["test-addons"],
label: "quickactions-addons",
onPick: () =>
@@ -32,7 +32,7 @@ add_setup(async function () {
"about:about"
),
});
- UrlbarProviderQuickActions.addAction("test-downloads", {
+ ActionsProviderQuickActions.addAction("test-downloads", {
commands: ["test-downloads"],
label: "quickactions-downloads2",
onPick: () =>
@@ -43,40 +43,22 @@ add_setup(async function () {
});
registerCleanupFunction(function () {
- UrlbarProviderQuickActions.removeAction("test-addons");
- UrlbarProviderQuickActions.removeAction("test-downloads");
+ ActionsProviderQuickActions.removeAction("test-addons");
+ ActionsProviderQuickActions.removeAction("test-downloads");
});
});
add_task(async function basic() {
const testData = [
{
- description: "Normal result to quick action button",
- mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
- mouseup: ".urlbarView-quickaction-button[data-key=test-downloads]",
- expected: "about:downloads",
- },
- {
- description: "Normal result to out of result",
- mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner",
- mouseup: "body",
- expected: false,
- },
- {
description: "Quick action button to normal result",
- mousedown: ".urlbarView-quickaction-button[data-key=test-addons]",
+ mousedown: ".urlbarView-action-btn[data-action=test-addons]",
mouseup: ".urlbarView-row:nth-child(1)",
expected: "https://example.com/?q=test",
},
{
- description: "Quick action button to quick action button",
- mousedown: ".urlbarView-quickaction-button[data-key=test-addons]",
- mouseup: ".urlbarView-quickaction-button[data-key=test-downloads]",
- expected: "about:downloads",
- },
- {
description: "Quick action button to out of result",
- mousedown: ".urlbarView-quickaction-button[data-key=test-downloads]",
+ mousedown: ".urlbarView-action-btn[data-action=test-addons]",
mouseup: "body",
expected: false,
},
@@ -148,7 +130,7 @@ add_task(async function outOfBrowser() {
},
{
description: "Quick action button to out of browser",
- mousedown: ".urlbarView-quickaction-button[data-key=test-addons]",
+ mousedown: ".urlbarView-action-btn[data-action=test-addons]",
},
];
@@ -204,22 +186,22 @@ add_task(async function withSelectionByKeyboard() {
mouseup: "body",
expected: {
selectedElementByKey:
- "#urlbar-results .urlbarView-quickaction-button[selected]",
+ "#urlbar-results .urlbarView-action-btn[selected]",
selectedElementAfterMouseDown:
- "#urlbar-results .urlbarView-quickaction-button[selected]",
+ "#urlbar-results .urlbarView-action-btn[selected]",
actionedPage: false,
},
},
{
- description: "Select normal result, then click on about:downloads",
- mousedown: ".urlbarView-quickaction-button[data-key=test-downloads]",
- mouseup: ".urlbarView-quickaction-button[data-key=test-downloads]",
+ description: "Select normal result, then click on about:addons",
+ mousedown: ".urlbarView-action-btn[data-action=test-addons]",
+ mouseup: ".urlbarView-action-btn[data-action=test-addons]",
expected: {
selectedElementByKey:
"#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]",
selectedElementAfterMouseDown:
- ".urlbarView-quickaction-button[data-key=test-downloads]",
- actionedPage: "about:downloads",
+ ".urlbarView-action-btn[data-action=test-addons]",
+ actionedPage: "about:about",
},
},
];
@@ -244,11 +226,7 @@ add_task(async function withSelectionByKeyboard() {
]);
if (arrowDown) {
- EventUtils.synthesizeKey(
- "KEY_ArrowDown",
- { repeat: arrowDown },
- window
- );
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: arrowDown }, window);
}
let [selectedElementByKey] = await waitForElements([
diff --git a/browser/components/urlbar/tests/browser/head.js b/browser/components/urlbar/tests/browser/head.js
index f78624e68e..73f9fda3a9 100644
--- a/browser/components/urlbar/tests/browser/head.js
+++ b/browser/components/urlbar/tests/browser/head.js
@@ -161,7 +161,7 @@ async function search({
// Set the input value and move the caret to the end to simulate the user
// typing. It's important the caret is at the end because otherwise autofill
// won't happen.
- gURLBar.value = searchString;
+ gURLBar._setValue(searchString, { allowTrim: false });
gURLBar.inputField.setSelectionRange(
searchString.length,
searchString.length
@@ -173,28 +173,21 @@ async function search({
// autofill before the search completes.
UrlbarTestUtils.fireInputEvent(window);
- // Subtract the protocol length, when the searchString contains the https://
- // protocol and trimHttps is enabled.
- let trimmedProtocolWSlashes = UrlbarTestUtils.getTrimmedProtocolWithSlashes();
- let selectionOffset = searchString.includes(trimmedProtocolWSlashes)
- ? trimmedProtocolWSlashes.length
- : 0;
-
// Check the input value and selection immediately, before waiting on the
// search to complete.
Assert.equal(
gURLBar.value,
- UrlbarTestUtils.trimURL(valueBefore),
+ valueBefore,
"gURLBar.value before the search completes"
);
Assert.equal(
gURLBar.selectionStart,
- searchString.length - selectionOffset,
+ searchString.length,
"gURLBar.selectionStart before the search completes"
);
Assert.equal(
gURLBar.selectionEnd,
- valueBefore.length - selectionOffset,
+ valueBefore.length,
"gURLBar.selectionEnd before the search completes"
);
@@ -205,17 +198,17 @@ async function search({
// Check the final value after the results arrived.
Assert.equal(
gURLBar.value,
- UrlbarTestUtils.trimURL(valueAfter),
+ valueAfter,
"gURLBar.value after the search completes"
);
Assert.equal(
gURLBar.selectionStart,
- searchString.length - selectionOffset,
+ searchString.length,
"gURLBar.selectionStart after the search completes"
);
Assert.equal(
gURLBar.selectionEnd,
- valueAfter.length - selectionOffset,
+ valueAfter.length,
"gURLBar.selectionEnd after the search completes"
);
@@ -227,7 +220,7 @@ async function search({
);
Assert.strictEqual(
gURLBar._autofillPlaceholder.value,
- UrlbarTestUtils.trimURL(placeholderAfter),
+ placeholderAfter,
"gURLBar._autofillPlaceholder.value after the search completes"
);
} else {
@@ -246,3 +239,51 @@ async function search({
"First result is an autofill result iff a placeholder is expected"
);
}
+
+function selectWithMouseDrag(fromX, toX, win = window) {
+ let target = win.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, win = window) {
+ let target = win.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/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_groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js
index ce69d30517..f930b28f59 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_groups.js
@@ -59,9 +59,9 @@ add_task(async function recent_search() {
assert: () =>
assertAbandonmentTelemetry([
{
- groups: "recent_search,suggested_index",
- results: "recent_search,action",
- n_results: 2,
+ groups: "recent_search",
+ results: "recent_search",
+ n_results: 1,
},
]),
});
@@ -114,9 +114,9 @@ add_task(async function top_site() {
assert: () =>
assertAbandonmentTelemetry([
{
- groups: "top_site,suggested_index",
- results: "top_site,action",
- n_results: 2,
+ groups: "top_site",
+ results: "top_site",
+ n_results: 1,
},
]),
});
@@ -128,9 +128,9 @@ add_task(async function clipboard() {
assert: () =>
assertAbandonmentTelemetry([
{
- groups: "general,suggested_index",
- results: "clipboard,action",
- n_results: 2,
+ groups: "general",
+ results: "clipboard",
+ n_results: 1,
},
]),
});
@@ -170,9 +170,9 @@ add_task(async function general() {
assert: () =>
assertAbandonmentTelemetry([
{
- groups: "heuristic,suggested_index,general",
- results: "search_engine,action,bookmark",
- n_results: 3,
+ groups: "heuristic,general",
+ results: "search_engine,bookmark",
+ n_results: 2,
},
]),
});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js
index 7edcc47a30..45f4b79e7c 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_abandonment_search_mode.js
@@ -45,10 +45,3 @@ add_task(async function tabs() {
assert: () => assertAbandonmentTelemetry([{ search_mode: "tabs" }]),
});
});
-
-add_task(async function actions() {
- await doActionsTest({
- trigger: () => doBlur(),
- assert: () => assertAbandonmentTelemetry([{ search_mode: "actions" }]),
- });
-});
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_edge_cases.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js
index 04ef7e9757..1668470714 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_edge_cases.js
@@ -196,7 +196,6 @@ add_task(async function enter_to_reload_current_url() {
await BrowserTestUtils.waitForCondition(
() => window.document.activeElement === gURLBar.inputField
);
- await UrlbarTestUtils.promiseSearchComplete(window);
// Press Enter key to reload the page without selecting any suggestions.
await doEnter();
@@ -213,8 +212,8 @@ add_task(async function enter_to_reload_current_url() {
selected_result: "input_field",
selected_result_subtype: "",
provider: undefined,
- results: "action",
- groups: "suggested_index",
+ results: "",
+ groups: "",
},
]);
});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js
index d46c874403..a0ef61dd19 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_groups.js
@@ -59,9 +59,9 @@ add_task(async function recent_search() {
assert: () =>
assertEngagementTelemetry([
{
- groups: "recent_search,suggested_index",
- results: "recent_search,action",
- n_results: 2,
+ groups: "recent_search",
+ results: "recent_search",
+ n_results: 1,
},
]),
});
@@ -114,9 +114,9 @@ add_task(async function top_site() {
assert: () =>
assertEngagementTelemetry([
{
- groups: "top_site,suggested_index",
- results: "top_site,action",
- n_results: 2,
+ groups: "top_site",
+ results: "top_site",
+ n_results: 1,
},
]),
});
@@ -128,9 +128,9 @@ add_task(async function clipboard() {
assert: () =>
assertEngagementTelemetry([
{
- groups: "general,suggested_index",
- results: "clipboard,action",
- n_results: 2,
+ groups: "general",
+ results: "clipboard",
+ n_results: 1,
},
]),
});
@@ -170,9 +170,9 @@ add_task(async function general() {
assert: () =>
assertEngagementTelemetry([
{
- groups: "heuristic,suggested_index,general",
- results: "search_engine,action,bookmark",
- n_results: 3,
+ groups: "heuristic,general",
+ results: "search_engine,bookmark",
+ n_results: 2,
},
]),
});
@@ -255,6 +255,7 @@ add_task(async function always_empty_if_drop_go() {
await doTest(async () => {
// Open the results view once.
+ await addTopSites("https://example.com/");
await showResultByArrowDown();
await UrlbarTestUtils.promisePopupClose(window);
@@ -282,6 +283,7 @@ add_task(async function always_empty_if_paste_go() {
await doTest(async () => {
// Open the results view once.
+ await addTopSites("https://example.com/");
await showResultByArrowDown();
await UrlbarTestUtils.promisePopupClose(window);
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js
index 2866186c30..d2deacf597 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_interaction.js
@@ -42,6 +42,7 @@ add_task(async function dropped() {
});
await doTest(async () => {
+ await addTopSites("https://example.com/");
await showResultByArrowDown();
await doDropAndGo("example.com");
@@ -67,6 +68,7 @@ add_task(async function pasted() {
});
await doTest(async () => {
+ await addTopSites("https://example.com/");
await showResultByArrowDown();
await doPasteAndGo("www.example.com");
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js
index 013bef1904..9227b81fd0 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_search_mode.js
@@ -42,6 +42,7 @@ add_task(async function tabs() {
await doTabTest({
trigger: async () => {
const currentTab = gBrowser.selectedTab;
+ EventUtils.synthesizeKey("KEY_Tab");
EventUtils.synthesizeKey("KEY_Enter");
await BrowserTestUtils.waitForCondition(
() => gBrowser.selectedTab !== currentTab
@@ -50,14 +51,3 @@ add_task(async function tabs() {
assert: () => assertEngagementTelemetry([{ search_mode: "tabs" }]),
});
});
-
-add_task(async function actions() {
- await doActionsTest({
- trigger: async () => {
- const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
- doClickSubButton(".urlbarView-quickaction-button[data-key=addons]");
- await onLoad;
- },
- assert: () => assertEngagementTelemetry([{ search_mode: "actions" }]),
- });
-});
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js
index bea266dbf4..34083e4369 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/browser_glean_telemetry_engagement_selected_result.js
@@ -119,9 +119,9 @@ add_task(async function selected_result_bookmark() {
{
selected_result: "bookmark",
selected_result_subtype: "",
- selected_position: 3,
+ selected_position: 2,
provider: "Places",
- results: "search_engine,action,bookmark",
+ results: "search_engine,bookmark",
},
]);
});
@@ -267,27 +267,11 @@ add_task(async function selected_result_url() {
});
});
-add_task(async function selected_result_action() {
- await doTest(async () => {
- await showResultByArrowDown();
- await selectRowByProvider("quickactions");
- const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
- doClickSubButton(".urlbarView-quickaction-button[data-key=addons]");
- await onLoad;
-
- assertEngagementTelemetry([
- {
- selected_result: "action",
- selected_result_subtype: "addons",
- selected_position: 1,
- provider: "quickactions",
- results: "action",
- },
- ]);
+add_task(async function selected_result_tab() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.secondaryActions.featureGate", false]],
});
-});
-add_task(async function selected_result_tab() {
const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com/");
await doTest(async () => {
@@ -307,6 +291,7 @@ add_task(async function selected_result_tab() {
]);
});
+ await SpecialPowers.popPrefEnv();
BrowserTestUtils.removeTab(tab);
});
@@ -402,7 +387,7 @@ add_task(async function selected_result_top_site() {
selected_result_subtype: "",
selected_position: 1,
provider: "UrlbarProviderTopSites",
- results: "top_site,action",
+ results: "top_site",
},
]);
});
@@ -456,7 +441,7 @@ add_task(async function selected_result_clipboard() {
selected_result_subtype: "",
selected_position: 1,
provider: "UrlbarProviderClipboard",
- results: "clipboard,action",
+ results: "clipboard",
},
]);
});
@@ -492,50 +477,6 @@ add_task(async function selected_result_unit() {
await SpecialPowers.popPrefEnv();
});
-add_task(async function selected_result_site_specific_contextual_search() {
- await SpecialPowers.pushPrefEnv({
- set: [["browser.urlbar.contextualSearch.enabled", true]],
- });
-
- await doTest(async () => {
- const extension = await SearchTestUtils.installSearchExtension(
- {
- name: "Contextual",
- search_url: "https://example.com/browser",
- },
- { skipUnload: true }
- );
- const onLoaded = BrowserTestUtils.browserLoaded(
- gBrowser.selectedBrowser,
- false,
- "https://example.com/"
- );
- BrowserTestUtils.startLoadingURIString(
- gBrowser.selectedBrowser,
- "https://example.com/"
- );
- await onLoaded;
-
- await openPopup("search");
- await selectRowByProvider("UrlbarProviderContextualSearch");
- await doEnter();
-
- assertEngagementTelemetry([
- {
- selected_result: "site_specific_contextual_search",
- selected_result_subtype: "",
- selected_position: 2,
- provider: "UrlbarProviderContextualSearch",
- results: "search_engine,site_specific_contextual_search",
- },
- ]);
-
- await extension.unload();
- });
-
- await SpecialPowers.popPrefEnv();
-});
-
add_task(async function selected_result_rs_adm_sponsored() {
const cleanupQuickSuggest = await ensureQuickSuggestInit({
prefs: [["quicksuggest.rustEnabled", false]],
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-interaction.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js
index 58c55b416f..dffdebad97 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-interaction.js
@@ -130,6 +130,7 @@ async function doTypedTest({ trigger, assert }) {
async function doTypedWithResultsPopupTest({ trigger, assert }) {
await doTest(async () => {
+ await addTopSites("https://example.org/");
await showResultByArrowDown();
EventUtils.synthesizeKey("x");
await UrlbarTestUtils.promiseSearchComplete(window);
@@ -150,6 +151,7 @@ async function doPastedTest({ trigger, assert }) {
async function doPastedWithResultsPopupTest({ trigger, assert }) {
await doTest(async () => {
+ await addTopSites("https://example.org/");
await showResultByArrowDown();
await doPaste("x");
@@ -257,6 +259,7 @@ async function doPersistedSearchTermsRestartedRefinedTest({
for (const { firstInput, secondInput, expected } of testData) {
await doTest(async () => {
+ await addTopSites("https://example.com/");
await openPopup(firstInput);
await doEnter();
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js
index 86151e1ba3..25a6504109 100644
--- a/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js
+++ b/browser/components/urlbar/tests/engagementTelemetry/browser/head-search_mode.js
@@ -78,16 +78,3 @@ async function doTabTest({ trigger, assert }) {
BrowserTestUtils.removeTab(tab);
}
-
-async function doActionsTest({ trigger, assert }) {
- await doTest(async () => {
- await openPopup("add");
- await UrlbarTestUtils.enterSearchMode(window, {
- source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
- });
- await selectRowByProvider("quickactions");
-
- await trigger();
- await assert();
- });
-}
diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head.js
index 4317a50930..a3b2b020c0 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() {
@@ -410,9 +406,7 @@ async function setup() {
set: [
["browser.urlbar.searchEngagementTelemetry.enabled", true],
["browser.urlbar.quickactions.enabled", true],
- ["browser.urlbar.quickactions.minimumSearchString", 0],
- ["browser.urlbar.suggest.quickactions", true],
- ["browser.urlbar.shortcuts.quickactions", true],
+ ["browser.urlbar.secondaryActions.featureGate", true],
],
});
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..87f94a641b 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js
@@ -34,6 +34,27 @@ const REMOTE_SETTINGS_RESULTS = [
},
];
+const MERINO_NAVIGATIONAL_SUGGESTION = {
+ url: "https://example.com/navigational-suggestion",
+ title: "Navigational suggestion",
+ provider: "top_picks",
+ is_sponsored: false,
+ score: 0.25,
+ block_id: 0,
+ is_top_pick: true,
+};
+
+const MERINO_DYNAMIC_WIKIPEDIA_SUGGESTION = {
+ url: "https://example.com/dynamic-wikipedia",
+ title: "Dynamic Wikipedia suggestion",
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "dynamic-wikipedia",
+ provider: "wikipedia",
+ iab_category: "5 - Education",
+ block_id: 1,
+};
+
add_setup(async function () {
await PlacesUtils.history.clear();
await PlacesUtils.bookmarks.eraseEverything();
@@ -46,7 +67,11 @@ add_setup(async function () {
attachment: REMOTE_SETTINGS_RESULTS,
},
],
+ merinoSuggestions: [],
});
+
+ // Disable Merino so we trigger only remote settings suggestions.
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", false);
});
// Tests a sponsored result and keyword highlighting.
@@ -164,3 +189,51 @@ add_tasks_with_rust(
await cleanUpNimbus();
}
);
+
+// Tests the "Manage" result menu for sponsored suggestion.
+add_tasks_with_rust(async function resultMenu_manage_sponsored() {
+ await doManageTest({
+ input: "fra",
+ index: 1,
+ });
+});
+
+// Tests the "Manage" result menu for non-sponsored suggestion.
+add_tasks_with_rust(async function resultMenu_manage_nonSponsored() {
+ await doManageTest({
+ input: "nonspon",
+ index: 1,
+ });
+});
+
+// Tests the "Manage" result menu for Navigational suggestion.
+add_tasks_with_rust(async function resultMenu_manage_navigational() {
+ // Enable Merino.
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+ MerinoTestUtils.server.response.body.suggestions = [
+ MERINO_NAVIGATIONAL_SUGGESTION,
+ ];
+
+ await doManageTest({
+ input: "test",
+ index: 1,
+ });
+
+ UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
+});
+
+// Tests the "Manage" result menu for Dynamic Wikipedia suggestion.
+add_tasks_with_rust(async function resultMenu_manage_dynamicWikipedia() {
+ // Enable Merino.
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+ MerinoTestUtils.server.response.body.suggestions = [
+ MERINO_DYNAMIC_WIKIPEDIA_SUGGESTION,
+ ];
+
+ await doManageTest({
+ input: "test",
+ index: 1,
+ });
+
+ UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
+});
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..c659eee268 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 = {
@@ -93,24 +88,6 @@ add_task(async function () {
},
},
},
- // help
- {
- command: "help",
- scalars: {
- [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position,
- [TELEMETRY_SCALARS.HELP_DYNAMIC_WIKIPEDIA]: position,
- },
- event: {
- category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
- method: "engagement",
- object: "help",
- extra: {
- suggestion_type,
- match_type,
- position: position.toString(),
- },
- },
- },
],
});
});
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/browser_telemetry_weather.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js
index e87c64740f..90044a95bd 100644
--- a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js
@@ -121,24 +121,6 @@ add_task(async function () {
},
},
},
- // help
- {
- command: "help",
- scalars: {
- [WEATHER_SCALARS.IMPRESSION]: position,
- [WEATHER_SCALARS.HELP]: position,
- },
- event: {
- category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
- method: "engagement",
- object: "help",
- extra: {
- suggestion_type,
- match_type,
- position: position.toString(),
- },
- },
- },
],
});
});
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_dynamicWikipedia.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js
index a9f339c324..7fc687d3df 100644
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_dynamicWikipedia.js
@@ -90,14 +90,11 @@ function makeExpectedResult() {
qsSuggestion: "full_keyword",
source: "merino",
provider: "wikipedia",
- 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,
},
};
}
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_merino.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js
index 64f4991236..b88e14e0e0 100644
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_merino.js
@@ -549,14 +549,11 @@ add_task(async function bestMatch() {
url: "url",
icon: null,
qsSuggestion: "full_keyword",
- 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,
displayUrl: "url",
source: "merino",
provider: "some_top_pick_provider",
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_quicksuggest_scoreMap.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_scoreMap.js
index 224dd6cb22..89fefd3163 100644
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_scoreMap.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_scoreMap.js
@@ -657,14 +657,11 @@ function makeExpectedDefaultResult({ suggestion }) {
? { id: "urlbar-result-action-sponsored" }
: undefined,
shouldShowUrl: true,
- 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,
},
};
}
diff --git a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js
index 1b8da54920..468cedbe0b 100644
--- a/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js
+++ b/browser/components/urlbar/tests/quicksuggest/unit/test_quicksuggest_topPicks.js
@@ -175,14 +175,11 @@ function makeExpectedResult({
shouldShowUrl: true,
source: "merino",
provider: telemetryType,
- 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,
},
};
if (typeof dupedHeuristic == "boolean") {
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_autofill_origins.js b/browser/components/urlbar/tests/unit/test_autofill_origins.js
index 33e462a8af..454881b933 100644
--- a/browser/components/urlbar/tests/unit/test_autofill_origins.js
+++ b/browser/components/urlbar/tests/unit/test_autofill_origins.js
@@ -1039,3 +1039,68 @@ async function doTitleTest({ visits, input, expected }) {
await cleanup();
}
+
+/* Tests sorting order when only unvisited bookmarks are available (e.g. in
+ permanent private browsing mode), then the only information we have is the
+ number of bookmarks per origin, and we're going to use that. */
+add_task(async function just_multiple_unvisited_bookmarks() {
+ // These are sorted to avoid confusion with natural sorting, so the one with
+ // the highest score is added in the middle.
+ let filledUrl = "https://www.tld2.com/";
+ let urls = [
+ {
+ url: "https://tld1.com/",
+ count: 1,
+ },
+ {
+ url: "https://tld2.com/",
+ count: 2,
+ },
+ {
+ url: filledUrl,
+ count: 2,
+ },
+ {
+ url: "https://tld3.com/",
+ count: 3,
+ },
+ ];
+
+ await PlacesUtils.history.clear();
+ for (let { url, count } of urls) {
+ while (count--) {
+ await PlacesTestUtils.addBookmarkWithDetails({
+ uri: url,
+ });
+ }
+ }
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ let context = createContext("tld", { isPrivate: false });
+ await check_results({
+ context,
+ autofilled: "tld2.com/",
+ completed: filledUrl,
+ matches: [
+ makeVisitResult(context, {
+ uri: filledUrl,
+ title: "A bookmark",
+ heuristic: true,
+ }),
+ makeBookmarkResult(context, {
+ uri: "https://tld3.com/",
+ title: "A bookmark",
+ }),
+ makeBookmarkResult(context, {
+ uri: "https://tld2.com/",
+ title: "A bookmark",
+ }),
+ makeBookmarkResult(context, {
+ uri: "https://tld1.com/",
+ title: "A bookmark",
+ }),
+ ],
+ });
+
+ await cleanup();
+});
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/components/urlbar/tests/unit/test_quickactions.js b/browser/components/urlbar/tests/unit/test_quickactions.js
index 00206c77b2..30e3fbdd95 100644
--- a/browser/components/urlbar/tests/unit/test_quickactions.js
+++ b/browser/components/urlbar/tests/unit/test_quickactions.js
@@ -5,108 +5,58 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
- UrlbarProviderQuickActions:
- "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+ ActionsProviderQuickActions:
+ "resource:///modules/ActionsProviderQuickActions.sys.mjs",
});
-let expectedMatch = (key, inputLength) => ({
- type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
- source: UrlbarUtils.RESULT_SOURCE.ACTIONS,
- heuristic: false,
- payload: {
- results: [{ key }],
- dynamicType: "quickactions",
- inQuickActionsSearchMode: false,
- helpUrl: UrlbarProviderQuickActions.helpUrl,
- inputLength,
- },
-});
-
-testEngine_setup();
-
add_setup(async () => {
UrlbarPrefs.set("quickactions.enabled", true);
- UrlbarPrefs.set("suggest.quickactions", true);
- UrlbarProviderQuickActions.addAction("newaction", {
+ ActionsProviderQuickActions.addAction("newaction", {
commands: ["newaction"],
});
registerCleanupFunction(async () => {
UrlbarPrefs.clear("quickactions.enabled");
- UrlbarPrefs.clear("suggest.quickactions");
- UrlbarProviderQuickActions.removeAction("newaction");
+ ActionsProviderQuickActions.removeAction("newaction");
});
});
add_task(async function nomatch() {
- let context = createContext("this doesnt match", {
- providers: [UrlbarProviderQuickActions.name],
- isPrivate: false,
- });
- await check_results({
- context,
- matches: [],
- });
-});
-
-add_task(async function quickactions_disabled() {
- UrlbarPrefs.set("suggest.quickactions", false);
- let context = createContext("new", {
- providers: [UrlbarProviderQuickActions.name],
- isPrivate: false,
- });
- await check_results({
- context,
- matches: [],
- });
+ let context = createContext("this doesnt match", {});
+ let result = await ActionsProviderQuickActions.queryAction(context);
+ Assert.ok(result === null, "there were no matches");
});
add_task(async function quickactions_match() {
- UrlbarPrefs.set("suggest.quickactions", true);
- let context = createContext("new", {
- providers: [UrlbarProviderQuickActions.name],
- isPrivate: false,
- });
- await check_results({
- context,
- matches: [expectedMatch("newaction", 3)],
- });
+ let context = createContext("new", {});
+ let result = await ActionsProviderQuickActions.queryAction(context);
+ Assert.ok(result.key == "newaction", "Matched the new action");
});
add_task(async function duplicate_matches() {
- UrlbarProviderQuickActions.addAction("testaction", {
+ ActionsProviderQuickActions.addAction("testaction", {
commands: ["testaction", "test"],
});
- let context = createContext("testaction", {
- providers: [UrlbarProviderQuickActions.name],
- isPrivate: false,
- });
+ let context = createContext("test", {});
+ let result = await ActionsProviderQuickActions.queryAction(context);
- await check_results({
- context,
- matches: [expectedMatch("testaction", 10)],
- });
+ Assert.ok(result.key == "testaction", "Matched the test action");
- UrlbarProviderQuickActions.removeAction("testaction");
+ ActionsProviderQuickActions.removeAction("testaction");
});
add_task(async function remove_action() {
- UrlbarProviderQuickActions.addAction("testaction", {
+ ActionsProviderQuickActions.addAction("testaction", {
commands: ["testaction"],
});
- UrlbarProviderQuickActions.removeAction("testaction");
+ ActionsProviderQuickActions.removeAction("testaction");
- let context = createContext("test", {
- providers: [UrlbarProviderQuickActions.name],
- isPrivate: false,
- });
+ let context = createContext("test", {});
+ let result = await ActionsProviderQuickActions.queryAction(context);
- await check_results({
- context,
- matches: [],
- });
+ Assert.ok(result === null, "there were no matches");
});
add_task(async function minimum_search_string() {
@@ -114,13 +64,18 @@ add_task(async function minimum_search_string() {
for (let minimumSearchString of [0, 3]) {
UrlbarPrefs.set("quickactions.minimumSearchString", minimumSearchString);
for (let i = 1; i < 4; i++) {
- let context = createContext(searchString.substring(0, i), {
- providers: [UrlbarProviderQuickActions.name],
- isPrivate: false,
- });
- let matches =
- i >= minimumSearchString ? [expectedMatch("newaction", i)] : [];
- await check_results({ context, matches });
+ let context = createContext(searchString.substring(0, i), {});
+ let result = await ActionsProviderQuickActions.queryAction(context);
+
+ if (i >= minimumSearchString) {
+ Assert.ok(result.key == "newaction", "Matched the new action");
+ } else {
+ Assert.equal(
+ ActionsProviderQuickActions.isActive(context),
+ false,
+ "QuickActions Provider is not active"
+ );
+ }
}
}
UrlbarPrefs.clear("quickactions.minimumSearchString");
diff --git a/browser/components/urlbar/tests/unit/test_tokenizer.js b/browser/components/urlbar/tests/unit/test_tokenizer.js
index 835d1a5909..76b2c31ac2 100644
--- a/browser/components/urlbar/tests/unit/test_tokenizer.js
+++ b/browser/components/urlbar/tests/unit/test_tokenizer.js
@@ -32,6 +32,12 @@ add_task(async function test_tokenizer() {
],
},
{
+ desc: "do not separate restriction char at beginning in search mode",
+ searchMode: { engineName: "testEngine" },
+ searchString: `${UrlbarTokenizer.RESTRICT.SEARCH}test`,
+ expectedTokens: [{ value: "?test", type: UrlbarTokenizer.TYPE.TEXT }],
+ },
+ {
desc: "separate restriction char at end",
searchString: `test ${UrlbarTokenizer.RESTRICT.BOOKMARK}`,
expectedTokens: [
diff --git a/browser/config/mozconfigs/linux32/debug-fuzzing b/browser/config/mozconfigs/linux32/debug-fuzzing
index 1c1fcaccc8..bbd546d55d 100644
--- a/browser/config/mozconfigs/linux32/debug-fuzzing
+++ b/browser/config/mozconfigs/linux32/debug-fuzzing
@@ -7,6 +7,7 @@ export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/bin/llvm-symbolizer"
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
unset MOZ_STDCXX_COMPAT
diff --git a/browser/config/mozconfigs/linux32/nightly-fuzzing-asan b/browser/config/mozconfigs/linux32/nightly-fuzzing-asan
index 87eb7c6d81..1aca942d36 100644
--- a/browser/config/mozconfigs/linux32/nightly-fuzzing-asan
+++ b/browser/config/mozconfigs/linux32/nightly-fuzzing-asan
@@ -12,6 +12,7 @@ ac_add_options --enable-valgrind
. $topsrcdir/build/unix/mozconfig.asan
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
unset MOZ_STDCXX_COMPAT
diff --git a/browser/config/mozconfigs/linux64/debug-asan b/browser/config/mozconfigs/linux64/debug-asan
index f4f08643b1..af44144d6b 100644
--- a/browser/config/mozconfigs/linux64/debug-asan
+++ b/browser/config/mozconfigs/linux64/debug-asan
@@ -8,6 +8,8 @@ ac_add_options --enable-valgrind
. $topsrcdir/build/unix/mozconfig.asan
+ac_add_options --enable-gczeal
+
# Build with fuzzing support, so this build can also be used
# to analyze fuzzing bugs with rr.
ac_add_options --enable-fuzzing
diff --git a/browser/config/mozconfigs/linux64/debug-fuzzing b/browser/config/mozconfigs/linux64/debug-fuzzing
index c1b737cbb2..bd1c3d3e40 100644
--- a/browser/config/mozconfigs/linux64/debug-fuzzing
+++ b/browser/config/mozconfigs/linux64/debug-fuzzing
@@ -7,6 +7,7 @@ export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/bin/llvm-symbolizer"
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
unset MOZ_STDCXX_COMPAT
diff --git a/browser/config/mozconfigs/linux64/debug-fuzzing-noopt b/browser/config/mozconfigs/linux64/debug-fuzzing-noopt
index a3c94adfe1..7daf791cfd 100644
--- a/browser/config/mozconfigs/linux64/debug-fuzzing-noopt
+++ b/browser/config/mozconfigs/linux64/debug-fuzzing-noopt
@@ -7,6 +7,7 @@ export LLVM_SYMBOLIZER="$MOZ_FETCHES_DIR/llvm-symbolizer/bin/llvm-symbolizer"
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
unset MOZ_STDCXX_COMPAT
diff --git a/browser/config/mozconfigs/linux64/fuzzing-ccov b/browser/config/mozconfigs/linux64/fuzzing-ccov
index bd6d45d01f..c30ef141ad 100644
--- a/browser/config/mozconfigs/linux64/fuzzing-ccov
+++ b/browser/config/mozconfigs/linux64/fuzzing-ccov
@@ -9,6 +9,7 @@ ac_add_options --disable-jemalloc
ac_add_options --enable-debug-symbols=-g1
ac_add_options --enable-fuzzing
+ac_add_options --enable-gczeal
# Also, for consistency we disable the crash reporter and solely rely
# on libFuzzer to provide stacks both in the browser fuzzing case as
diff --git a/browser/config/mozconfigs/linux64/nightly-fuzzing-asan b/browser/config/mozconfigs/linux64/nightly-fuzzing-asan
index 114ced3ad6..d1b13e1ff7 100644
--- a/browser/config/mozconfigs/linux64/nightly-fuzzing-asan
+++ b/browser/config/mozconfigs/linux64/nightly-fuzzing-asan
@@ -14,6 +14,7 @@ ac_add_options --enable-valgrind
# globally in mozconfig.asan because it requires an unstable -Z flag.
export RUSTFLAGS="$RUSTFLAGS -Zsanitizer=address"
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
unset MOZ_STDCXX_COMPAT
diff --git a/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-afl b/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-afl
new file mode 100644
index 0000000000..2487523229
--- /dev/null
+++ b/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-afl
@@ -0,0 +1,9 @@
+. "$topsrcdir/browser/config/mozconfigs/linux64/nightly-fuzzing-asan"
+
+export CC="$MOZ_FETCHES_DIR/afl-instrumentation/bin/afl-clang-fast"
+export CXX="$MOZ_FETCHES_DIR/afl-instrumentation/bin/afl-clang-fast++"
+
+export HOST_CC="$MOZ_FETCHES_DIR/clang/bin/clang"
+export HOST_CXX="$MOZ_FETCHES_DIR/clang/bin/clang++"
+
+. "$topsrcdir/build/mozconfig.common.override"
diff --git a/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-noopt b/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-noopt
index 4743405afc..6359bad8e3 100644
--- a/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-noopt
+++ b/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-noopt
@@ -14,6 +14,7 @@ ac_add_options --enable-valgrind
# globally in mozconfig.asan because it requires an unstable -Z flag.
export RUSTFLAGS="$RUSTFLAGS -Zsanitizer=address"
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
unset MOZ_STDCXX_COMPAT
diff --git a/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-nyx b/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-nyx
index e970ce2576..01e90c305d 100644
--- a/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-nyx
+++ b/browser/config/mozconfigs/linux64/nightly-fuzzing-asan-nyx
@@ -20,6 +20,7 @@ ac_add_options --enable-valgrind
# globally in mozconfig.asan because it requires an unstable -Z flag.
export RUSTFLAGS="$RUSTFLAGS -Zsanitizer=address"
+ac_add_options --enable-gczeal
ac_add_options --enable-snapshot-fuzzing
unset MOZ_STDCXX_COMPAT
diff --git a/browser/config/mozconfigs/linux64/tsan-fuzzing b/browser/config/mozconfigs/linux64/tsan-fuzzing
index c7da44cd2c..b14e0ded32 100644
--- a/browser/config/mozconfigs/linux64/tsan-fuzzing
+++ b/browser/config/mozconfigs/linux64/tsan-fuzzing
@@ -1,4 +1,5 @@
. "$topsrcdir/browser/config/mozconfigs/linux64/tsan"
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
unset MOZ_STDCXX_COMPAT
diff --git a/browser/config/mozconfigs/macosx64/debug-fuzzing b/browser/config/mozconfigs/macosx64/debug-fuzzing
index 7a852d4350..9a5b2402f5 100644
--- a/browser/config/mozconfigs/macosx64/debug-fuzzing
+++ b/browser/config/mozconfigs/macosx64/debug-fuzzing
@@ -1,5 +1,6 @@
. "$topsrcdir/browser/config/mozconfigs/macosx64/debug"
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
# Need this to prevent name conflicts with the normal nightly build packages
diff --git a/browser/config/mozconfigs/macosx64/nightly-fuzzing-asan b/browser/config/mozconfigs/macosx64/nightly-fuzzing-asan
index 2b6ea4f61f..2ab08af6a2 100644
--- a/browser/config/mozconfigs/macosx64/nightly-fuzzing-asan
+++ b/browser/config/mozconfigs/macosx64/nightly-fuzzing-asan
@@ -1,5 +1,6 @@
. "$topsrcdir/browser/config/mozconfigs/macosx64/nightly-asan"
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
# Piggybacking UBSan for now since only a small subset of checks are enabled.
diff --git a/browser/config/mozconfigs/win32/debug-fuzzing b/browser/config/mozconfigs/win32/debug-fuzzing
index 874661253a..012d608436 100644
--- a/browser/config/mozconfigs/win32/debug-fuzzing
+++ b/browser/config/mozconfigs/win32/debug-fuzzing
@@ -3,6 +3,7 @@
# Disable telemetry. All network activity is undesirable in fuzzing.
ac_add_options MOZ_TELEMETRY_REPORTING=
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
# Need this to prevent name conflicts with the normal nightly build packages
diff --git a/browser/config/mozconfigs/win64/debug-fuzzing b/browser/config/mozconfigs/win64/debug-fuzzing
index a4d6931fe0..a523513354 100644
--- a/browser/config/mozconfigs/win64/debug-fuzzing
+++ b/browser/config/mozconfigs/win64/debug-fuzzing
@@ -3,6 +3,7 @@
# Disable telemetry. All network activity is undesirable in fuzzing.
ac_add_options MOZ_TELEMETRY_REPORTING=
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
# Need this to prevent name conflicts with the normal nightly build packages
diff --git a/browser/config/mozconfigs/win64/fuzzing-ccov b/browser/config/mozconfigs/win64/fuzzing-ccov
index d91f487dde..9cd1d69bd2 100644
--- a/browser/config/mozconfigs/win64/fuzzing-ccov
+++ b/browser/config/mozconfigs/win64/fuzzing-ccov
@@ -3,6 +3,7 @@
# Disable telemetry. All network activity is undesirable in fuzzing.
ac_add_options MOZ_TELEMETRY_REPORTING=
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
# Need this to prevent name conflicts with the normal nightly build packages
diff --git a/browser/config/mozconfigs/win64/nightly-fuzzing-asan b/browser/config/mozconfigs/win64/nightly-fuzzing-asan
index e0d9d0f178..d057d2783e 100644
--- a/browser/config/mozconfigs/win64/nightly-fuzzing-asan
+++ b/browser/config/mozconfigs/win64/nightly-fuzzing-asan
@@ -5,6 +5,7 @@ ac_add_options MOZ_TELEMETRY_REPORTING=
ac_add_options --disable-crashreporter
+ac_add_options --enable-gczeal
ac_add_options --enable-fuzzing
# Need this to prevent name conflicts with the normal nightly build packages
diff --git a/browser/config/version.txt b/browser/config/version.txt
index 61eb5d32fe..4aea959bf8 100644
--- a/browser/config/version.txt
+++ b/browser/config/version.txt
@@ -1 +1 @@
-125.0.3
+127.0
diff --git a/browser/config/version_display.txt b/browser/config/version_display.txt
index 61eb5d32fe..4aea959bf8 100644
--- a/browser/config/version_display.txt
+++ b/browser/config/version_display.txt
@@ -1 +1 @@
-125.0.3
+127.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/addressFormLayout.mjs b/browser/extensions/formautofill/content/addressFormLayout.mjs
new file mode 100644
index 0000000000..5e48e6afaa
--- /dev/null
+++ b/browser/extensions/formautofill/content/addressFormLayout.mjs
@@ -0,0 +1,187 @@
+/* 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, {
+ FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+});
+
+// Defines template descriptors for generating elements in convertLayoutToUI.
+const fieldTemplates = {
+ commonAttributes(item) {
+ return {
+ id: item.fieldId,
+ name: item.fieldId,
+ required: item.required,
+ value: item.value ?? "",
+ };
+ },
+ input(item) {
+ return {
+ tag: "input",
+ type: item.type ?? "text",
+ ...this.commonAttributes(item),
+ };
+ },
+ textarea(item) {
+ return {
+ tag: "textarea",
+ ...this.commonAttributes(item),
+ };
+ },
+ select(item) {
+ return {
+ tag: "select",
+ children: item.options.map(({ value, text }) => ({
+ tag: "option",
+ selected: value === item.value,
+ value,
+ text,
+ })),
+ ...this.commonAttributes(item),
+ };
+ },
+};
+
+/**
+ * Creates an HTML element with specified attributes and children.
+ *
+ * @param {string} tag - Tag name for the element to create.
+ * @param {object} options - Options object containing attributes and children.
+ * @param {object} options.attributes - Element's Attributes/Props (id, class, etc.)
+ * @param {Array} options.children - Element's children (array of objects with tag and options).
+ * @returns {HTMLElement} The newly created element.
+ */
+const createElement = (tag, { children = [], ...attributes }) => {
+ const element = document.createElement(tag);
+
+ for (let [attributeName, attributeValue] of Object.entries(attributes)) {
+ if (attributeName in element) {
+ element[attributeName] = attributeValue;
+ } else {
+ element.setAttribute(attributeName, attributeValue);
+ }
+ }
+
+ for (let { tag: childTag, ...childRest } of children) {
+ element.appendChild(createElement(childTag, childRest));
+ }
+
+ return element;
+};
+
+/**
+ * Generator that creates UI elements from `fields` object, using localization from `l10nStrings`.
+ *
+ * @param {Array} fields - Array of objects as returned from `FormAutofillUtils.getFormLayout`.
+ * @param {object} l10nStrings - Key-value pairs for field label localization.
+ * @yields {HTMLElement} - A localized label element with constructed from a field.
+ */
+function* convertLayoutToUI(fields, l10nStrings) {
+ for (const item of fields) {
+ // eslint-disable-next-line no-nested-ternary
+ const fieldTag = item.options
+ ? "select"
+ : item.multiline
+ ? "textarea"
+ : "input";
+
+ const fieldUI = {
+ label: {
+ id: `${item.fieldId}-container`,
+ class: `container ${item.newLine ? "new-line" : ""}`,
+ },
+ field: fieldTemplates[fieldTag](item),
+ span: {
+ class: "label-text",
+ textContent: l10nStrings[item.l10nId] ?? "",
+ },
+ };
+
+ const label = createElement("label", fieldUI.label);
+ const { tag, ...rest } = fieldUI.field;
+ const field = createElement(tag, rest);
+ label.appendChild(field);
+ const span = createElement("span", fieldUI.span);
+ label.appendChild(span);
+
+ yield label;
+ }
+}
+
+/**
+ * Retrieves the current form data from the current form element on the page.
+ *
+ * @returns {object} An object containing key-value pairs of form data.
+ */
+export const getCurrentFormData = () => {
+ const formElement = document.querySelector("form");
+ const formData = new FormData(formElement);
+ return Object.fromEntries(formData.entries());
+};
+
+/**
+ * Checks if the form can be submitted based on the number of non-empty values.
+ * TODO(Bug 1891734): Add address validation. Right now we don't do any validation. (2 fields mimics the old behaviour ).
+ *
+ * @returns {boolean} True if the form can be submitted
+ */
+export const canSubmitForm = () => {
+ const formData = getCurrentFormData();
+ const validValues = Object.values(formData).filter(Boolean);
+ return validValues.length >= 2;
+};
+
+/**
+ * Generates a form layout based on record data and localization strings.
+ *
+ * @param {HTMLFormElement} formElement - Target form element.
+ * @param {object} record - Address record, includes at least country code defaulted to FormAutofill.DEFAULT_REGION.
+ * @param {object} l10nStrings - Localization strings map.
+ */
+export const createFormLayoutFromRecord = (
+ formElement,
+ record = { country: lazy.FormAutofill.DEFAULT_REGION },
+ l10nStrings = {}
+) => {
+ // Always clear select values because they are not persisted between countries.
+ // For example from US with state NY, we don't want the address-level1 to be NY
+ // when changing to another country that doesn't have state options
+ const selects = formElement.querySelectorAll("select:not(#country)");
+ for (const select of selects) {
+ select.value = "";
+ }
+
+ // Get old data to persist before clearing form
+ const formData = getCurrentFormData();
+ record = {
+ ...record,
+ ...formData,
+ };
+
+ formElement.innerHTML = "";
+ const fields = lazy.FormAutofillUtils.getFormLayout(record);
+
+ const layoutGenerator = convertLayoutToUI(fields, l10nStrings);
+
+ for (const fieldElement of layoutGenerator) {
+ formElement.appendChild(fieldElement);
+ }
+
+ document.querySelector("#country").addEventListener(
+ "change",
+ ev =>
+ // Allow some time for the user to type
+ // before we set the new country and re-render
+ setTimeout(() => {
+ record.country = ev.target.value;
+ createFormLayoutFromRecord(formElement, record, l10nStrings);
+ }, 300),
+ { once: true }
+ );
+
+ // Used to notify tests that the form has been updated and is ready
+ window.dispatchEvent(new CustomEvent("FormReadyForTests"));
+};
diff --git a/browser/extensions/formautofill/content/autofillEditForms.js b/browser/extensions/formautofill/content/autofillEditForms.js
deleted file mode 100644
index 290b436a64..0000000000
--- a/browser/extensions/formautofill/content/autofillEditForms.js
+++ /dev/null
@@ -1,640 +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/. */
-
-/* exported EditAddress, EditCreditCard */
-/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
-
-"use strict";
-
-const { FormAutofill } = ChromeUtils.importESModule(
- "resource://autofill/FormAutofill.sys.mjs"
-);
-const { FormAutofillUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
-);
-
-class EditAutofillForm {
- constructor(elements) {
- this._elements = elements;
- }
-
- /**
- * Fill the form with a record object.
- *
- * @param {object} [record = {}]
- */
- loadRecord(record = {}) {
- for (let field of this._elements.form.elements) {
- let value = record[field.id];
- value = typeof value == "undefined" ? "" : value;
-
- if (record.guid) {
- field.value = value;
- } else if (field.localName == "select") {
- this.setDefaultSelectedOptionByValue(field, value);
- } else {
- // Use .defaultValue instead of .value to avoid setting the `dirty` flag
- // which triggers form validation UI.
- field.defaultValue = value;
- }
- }
- if (!record.guid) {
- // Reset the dirty value flag and validity state.
- this._elements.form.reset();
- } else {
- for (let field of this._elements.form.elements) {
- this.updatePopulatedState(field);
- this.updateCustomValidity(field);
- }
- }
- }
-
- setDefaultSelectedOptionByValue(select, value) {
- for (let option of select.options) {
- option.defaultSelected = option.value == value;
- }
- }
-
- /**
- * Get a record from the form suitable for a save/update in storage.
- *
- * @returns {object}
- */
- buildFormObject() {
- let initialObject = {};
- if (this.hasMailingAddressFields) {
- // Start with an empty string for each mailing-address field so that any
- // fields hidden for the current country are blanked in the return value.
- initialObject = {
- "street-address": "",
- "address-level3": "",
- "address-level2": "",
- "address-level1": "",
- "postal-code": "",
- };
- }
-
- return Array.from(this._elements.form.elements).reduce((obj, input) => {
- if (!input.disabled) {
- obj[input.id] = input.value;
- }
- return obj;
- }, initialObject);
- }
-
- /**
- * Handle events
- *
- * @param {DOMEvent} event
- */
- handleEvent(event) {
- switch (event.type) {
- case "change": {
- this.handleChange(event);
- break;
- }
- case "input": {
- this.handleInput(event);
- break;
- }
- }
- }
-
- /**
- * Handle change events
- *
- * @param {DOMEvent} event
- */
- handleChange(event) {
- this.updatePopulatedState(event.target);
- }
-
- /**
- * Handle input events
- */
- handleInput(_e) {}
-
- /**
- * Attach event listener
- */
- attachEventListeners() {
- this._elements.form.addEventListener("input", this);
- }
-
- /**
- * Set the field-populated attribute if the field has a value.
- *
- * @param {DOMElement} field The field that will be checked for a value.
- */
- updatePopulatedState(field) {
- let span = field.parentNode.querySelector(".label-text");
- if (!span) {
- return;
- }
- span.toggleAttribute("field-populated", !!field.value.trim());
- }
-
- /**
- * Run custom validity routines specific to the field and type of form.
- *
- * @param {DOMElement} _field The field that will be validated.
- */
- updateCustomValidity(_field) {}
-}
-
-class EditAddress extends EditAutofillForm {
- /**
- * @param {HTMLElement[]} elements
- * @param {object} record
- * @param {object} config
- * @param {boolean} [config.noValidate=undefined] Whether to validate the form
- */
- constructor(elements, record, config) {
- super(elements);
-
- Object.assign(this, config);
- let { form } = this._elements;
- Object.assign(this._elements, {
- addressLevel3Label: form.querySelector(
- "#address-level3-container > .label-text"
- ),
- addressLevel2Label: form.querySelector(
- "#address-level2-container > .label-text"
- ),
- addressLevel1Label: form.querySelector(
- "#address-level1-container > .label-text"
- ),
- postalCodeLabel: form.querySelector(
- "#postal-code-container > .label-text"
- ),
- country: form.querySelector("#country"),
- });
-
- this.populateCountries();
- // Need to populate the countries before trying to set the initial country.
- // Also need to use this._record so it has the default country selected.
- this.loadRecord(record);
- this.attachEventListeners();
-
- form.noValidate = !!config.noValidate;
- }
-
- loadRecord(record) {
- this._record = record;
- if (!record) {
- record = {
- country: FormAutofill.DEFAULT_REGION,
- };
- }
-
- let { addressLevel1Options } = FormAutofillUtils.getFormFormat(
- record.country
- );
- this.populateAddressLevel1(addressLevel1Options, record.country);
-
- super.loadRecord(record);
- this.loadAddressLevel1(record["address-level1"], record.country);
- this.formatForm(record.country);
- }
-
- get hasMailingAddressFields() {
- let { addressFields } = this._elements.form.dataset;
- return (
- !addressFields ||
- addressFields.trim().split(/\s+/).includes("mailing-address")
- );
- }
-
- /**
- * `mailing-address` is a special attribute token to indicate mailing fields + country.
- *
- * @param {object[]} mailingFieldsOrder - `fieldsOrder` from `getFormFormat`
- * @param {string} addressFields - white-space-separated string of requested address fields to show
- * @returns {object[]} in the same structure as `mailingFieldsOrder` but including non-mail fields
- */
- static computeVisibleFields(mailingFieldsOrder, addressFields) {
- if (addressFields) {
- let requestedFieldClasses = addressFields.trim().split(/\s+/);
- let fieldClasses = [];
- if (requestedFieldClasses.includes("mailing-address")) {
- fieldClasses = fieldClasses.concat(mailingFieldsOrder);
- // `country` isn't part of the `mailingFieldsOrder` so add it when filling a mailing-address
- requestedFieldClasses.splice(
- requestedFieldClasses.indexOf("mailing-address"),
- 1,
- "country"
- );
- }
-
- for (let fieldClassName of requestedFieldClasses) {
- fieldClasses.push({
- fieldId: fieldClassName,
- newLine: fieldClassName == "name",
- });
- }
- return fieldClasses;
- }
-
- // This is the default which is shown in the management interface and includes all fields.
- return mailingFieldsOrder.concat([
- {
- fieldId: "country",
- },
- {
- fieldId: "tel",
- },
- {
- fieldId: "email",
- newLine: true,
- },
- ]);
- }
-
- /**
- * Format the form based on country. The address-level1 and postal-code labels
- * should be specific to the given country.
- *
- * @param {string} country
- */
- formatForm(country) {
- const {
- addressLevel3L10nId,
- addressLevel2L10nId,
- addressLevel1L10nId,
- addressLevel1Options,
- postalCodeL10nId,
- fieldsOrder: mailingFieldsOrder,
- postalCodePattern,
- countryRequiredFields,
- } = FormAutofillUtils.getFormFormat(country);
-
- document.l10n.setAttributes(
- this._elements.addressLevel3Label,
- addressLevel3L10nId
- );
- document.l10n.setAttributes(
- this._elements.addressLevel2Label,
- addressLevel2L10nId
- );
- document.l10n.setAttributes(
- this._elements.addressLevel1Label,
- addressLevel1L10nId
- );
- document.l10n.setAttributes(
- this._elements.postalCodeLabel,
- postalCodeL10nId
- );
- let addressFields = this._elements.form.dataset.addressFields;
- let extraRequiredFields = this._elements.form.dataset.extraRequiredFields;
- let fieldClasses = EditAddress.computeVisibleFields(
- mailingFieldsOrder,
- addressFields
- );
- let requiredFields = new Set(countryRequiredFields);
- if (extraRequiredFields) {
- for (let extraRequiredField of extraRequiredFields.trim().split(/\s+/)) {
- requiredFields.add(extraRequiredField);
- }
- }
- this.arrangeFields(fieldClasses, requiredFields);
- this.updatePostalCodeValidation(postalCodePattern);
- this.populateAddressLevel1(addressLevel1Options, country);
- }
-
- /**
- * Update address field visibility and order based on libaddressinput data.
- *
- * @param {object[]} fieldsOrder array of objects with `fieldId` and optional `newLine` properties
- * @param {Set} requiredFields Set of `fieldId` strings that mark which fields are required
- */
- arrangeFields(fieldsOrder, requiredFields) {
- /**
- * @see FormAutofillStorage.VALID_ADDRESS_FIELDS
- */
- let fields = [
- // `name` is a wrapper for the 3 name fields.
- "name",
- "organization",
- "street-address",
- "address-level3",
- "address-level2",
- "address-level1",
- "postal-code",
- "country",
- "tel",
- "email",
- ];
- let inputs = [];
- for (let i = 0; i < fieldsOrder.length; i++) {
- let { fieldId, newLine } = fieldsOrder[i];
-
- let container = this._elements.form.querySelector(
- `#${fieldId}-container`
- );
- let containerInputs = [
- ...container.querySelectorAll("input, textarea, select"),
- ];
- containerInputs.forEach(function (input) {
- input.disabled = false;
- // libaddressinput doesn't list 'country' or 'name' as required.
- input.required =
- fieldId == "country" ||
- fieldId == "name" ||
- requiredFields.has(fieldId);
- });
- inputs.push(...containerInputs);
- container.style.display = "flex";
- container.style.order = i;
- container.style.pageBreakAfter = newLine ? "always" : "auto";
- // Remove the field from the list of fields
- fields.splice(fields.indexOf(fieldId), 1);
- }
- for (let i = 0; i < inputs.length; i++) {
- // Assign tabIndex starting from 1
- inputs[i].tabIndex = i + 1;
- }
- // Hide the remaining fields
- for (let field of fields) {
- let container = this._elements.form.querySelector(`#${field}-container`);
- container.style.display = "none";
- for (let input of [
- ...container.querySelectorAll("input, textarea, select"),
- ]) {
- input.disabled = true;
- }
- }
- }
-
- updatePostalCodeValidation(postalCodePattern) {
- let postalCodeInput = this._elements.form.querySelector("#postal-code");
- if (postalCodePattern && postalCodeInput.style.display != "none") {
- postalCodeInput.setAttribute("pattern", postalCodePattern);
- } else {
- postalCodeInput.removeAttribute("pattern");
- }
- }
-
- /**
- * Set the address-level1 value on the form field (input or select, whichever is present).
- *
- * @param {string} addressLevel1Value Value of the address-level1 from the autofill record
- * @param {string} country The corresponding country
- */
- loadAddressLevel1(addressLevel1Value, country) {
- let field = this._elements.form.querySelector("#address-level1");
-
- if (field.localName == "input") {
- field.value = addressLevel1Value || "";
- return;
- }
-
- let matchedSelectOption = FormAutofillUtils.findAddressSelectOption(
- field,
- {
- country,
- "address-level1": addressLevel1Value,
- },
- "address-level1"
- );
- if (matchedSelectOption && !matchedSelectOption.selected) {
- field.value = matchedSelectOption.value;
- field.dispatchEvent(new Event("input", { bubbles: true }));
- field.dispatchEvent(new Event("change", { bubbles: true }));
- } else if (addressLevel1Value) {
- // If the option wasn't found, insert an option at the beginning of
- // the select that matches the stored value.
- field.insertBefore(
- new Option(addressLevel1Value, addressLevel1Value, true, true),
- field.firstChild
- );
- }
- }
-
- /**
- * Replace the text input for address-level1 with a select dropdown if
- * a fixed set of names exists. Otherwise show a text input.
- *
- * @param {Map?} options Map of options with regionCode -> name mappings
- * @param {string} country The corresponding country
- */
- populateAddressLevel1(options, country) {
- let field = this._elements.form.querySelector("#address-level1");
-
- if (field.dataset.country == country) {
- return;
- }
-
- if (!options) {
- if (field.localName == "input") {
- return;
- }
-
- let input = document.createElement("input");
- input.setAttribute("type", "text");
- input.id = "address-level1";
- input.required = field.required;
- input.disabled = field.disabled;
- input.tabIndex = field.tabIndex;
- field.replaceWith(input);
- return;
- }
-
- if (field.localName == "input") {
- let select = document.createElement("select");
- select.id = "address-level1";
- select.required = field.required;
- select.disabled = field.disabled;
- select.tabIndex = field.tabIndex;
- field.replaceWith(select);
- field = select;
- }
-
- field.textContent = "";
- field.dataset.country = country;
- let fragment = document.createDocumentFragment();
- fragment.appendChild(new Option(undefined, undefined, true, true));
- for (let [regionCode, regionName] of options) {
- let option = new Option(regionName, regionCode);
- fragment.appendChild(option);
- }
- field.appendChild(fragment);
- }
-
- populateCountries() {
- let fragment = document.createDocumentFragment();
- // Sort countries by their visible names.
- let countries = [...FormAutofill.countries.entries()].sort((e1, e2) =>
- e1[1].localeCompare(e2[1])
- );
- for (let [country] of countries) {
- const countryName = Services.intl.getRegionDisplayNames(undefined, [
- country.toLowerCase(),
- ]);
- const option = new Option(countryName, country);
- fragment.appendChild(option);
- }
- this._elements.country.appendChild(fragment);
- }
-
- handleChange(event) {
- if (event.target == this._elements.country) {
- this.formatForm(event.target.value);
- }
- super.handleChange(event);
- }
-
- attachEventListeners() {
- this._elements.form.addEventListener("change", this);
- super.attachEventListeners();
- }
-}
-
-class EditCreditCard extends EditAutofillForm {
- /**
- * @param {HTMLElement[]} elements
- * @param {object} record with a decrypted cc-number
- * @param {object} addresses in an object with guid keys for the billing address picker.
- */
- constructor(elements, record, addresses) {
- super(elements);
-
- this._addresses = addresses;
- Object.assign(this._elements, {
- ccNumber: this._elements.form.querySelector("#cc-number"),
- invalidCardNumberStringElement: this._elements.form.querySelector(
- "#invalidCardNumberString"
- ),
- month: this._elements.form.querySelector("#cc-exp-month"),
- year: this._elements.form.querySelector("#cc-exp-year"),
- billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
- billingAddressRow:
- this._elements.form.querySelector(".billingAddressRow"),
- });
-
- this.attachEventListeners();
- this.loadRecord(record, addresses);
- }
-
- loadRecord(record, addresses, preserveFieldValues) {
- // _record must be updated before generateYears and generateBillingAddressOptions are called.
- this._record = record;
- this._addresses = addresses;
- this.generateBillingAddressOptions(preserveFieldValues);
- if (!preserveFieldValues) {
- // Re-generating the months will reset the selected option.
- this.generateMonths();
- // Re-generating the years will reset the selected option.
- this.generateYears();
- super.loadRecord(record);
- }
- }
-
- generateMonths() {
- const count = 12;
-
- // Clear the list
- this._elements.month.textContent = "";
-
- // Empty month option
- this._elements.month.appendChild(new Option());
-
- // Populate month list. Format: "month number - month name"
- let dateFormat = new Intl.DateTimeFormat(navigator.language, {
- month: "long",
- }).format;
- for (let i = 0; i < count; i++) {
- let monthNumber = (i + 1).toString();
- let monthName = dateFormat(new Date(1970, i));
- let option = new Option();
- option.value = monthNumber;
- // XXX: Bug 1446164 - Localize this string.
- option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
- this._elements.month.appendChild(option);
- }
- }
-
- generateYears() {
- const count = 11;
- const currentYear = new Date().getFullYear();
- const ccExpYear = this._record && this._record["cc-exp-year"];
-
- // Clear the list
- this._elements.year.textContent = "";
-
- // Provide an empty year option
- this._elements.year.appendChild(new Option());
-
- if (ccExpYear && ccExpYear < currentYear) {
- this._elements.year.appendChild(new Option(ccExpYear));
- }
-
- for (let i = 0; i < count; i++) {
- let year = currentYear + i;
- let option = new Option(year);
- this._elements.year.appendChild(option);
- }
-
- if (ccExpYear && ccExpYear > currentYear + count) {
- this._elements.year.appendChild(new Option(ccExpYear));
- }
- }
-
- generateBillingAddressOptions(preserveFieldValues) {
- let billingAddressGUID;
- if (preserveFieldValues && this._elements.billingAddress.value) {
- billingAddressGUID = this._elements.billingAddress.value;
- } else if (this._record) {
- billingAddressGUID = this._record.billingAddressGUID;
- }
-
- this._elements.billingAddress.textContent = "";
-
- this._elements.billingAddress.appendChild(new Option("", ""));
-
- let hasAddresses = false;
- for (let [guid, address] of Object.entries(this._addresses)) {
- hasAddresses = true;
- let selected = guid == billingAddressGUID;
- let option = new Option(
- FormAutofillUtils.getAddressLabel(address),
- guid,
- selected,
- selected
- );
- this._elements.billingAddress.appendChild(option);
- }
-
- this._elements.billingAddressRow.hidden = !hasAddresses;
- }
-
- attachEventListeners() {
- this._elements.form.addEventListener("change", this);
- super.attachEventListeners();
- }
-
- handleInput(event) {
- // Clear the error message if cc-number is valid
- if (
- event.target == this._elements.ccNumber &&
- FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
- ) {
- this._elements.ccNumber.setCustomValidity("");
- }
- super.handleInput(event);
- }
-
- updateCustomValidity(field) {
- super.updateCustomValidity(field);
-
- // Mark the cc-number field as invalid if the number is empty or invalid.
- if (
- field == this._elements.ccNumber &&
- !FormAutofillUtils.isCCNumber(field.value)
- ) {
- let invalidCardNumberString =
- this._elements.invalidCardNumberStringElement.textContent;
- field.setCustomValidity(invalidCardNumberString || " ");
- }
- }
-}
diff --git a/browser/extensions/formautofill/content/autofillEditForms.mjs b/browser/extensions/formautofill/content/autofillEditForms.mjs
new file mode 100644
index 0000000000..ca74850acd
--- /dev/null
+++ b/browser/extensions/formautofill/content/autofillEditForms.mjs
@@ -0,0 +1,288 @@
+/* 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 mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+});
+
+class EditAutofillForm {
+ constructor(elements) {
+ this._elements = elements;
+ }
+
+ /**
+ * Fill the form with a record object.
+ *
+ * @param {object} [record = {}]
+ */
+ loadRecord(record = {}) {
+ for (let field of this._elements.form.elements) {
+ let value = record[field.id];
+ value = typeof value == "undefined" ? "" : value;
+
+ if (record.guid) {
+ field.value = value;
+ } else if (field.localName == "select") {
+ this.setDefaultSelectedOptionByValue(field, value);
+ } else {
+ // Use .defaultValue instead of .value to avoid setting the `dirty` flag
+ // which triggers form validation UI.
+ field.defaultValue = value;
+ }
+ }
+ if (!record.guid) {
+ // Reset the dirty value flag and validity state.
+ this._elements.form.reset();
+ } else {
+ for (let field of this._elements.form.elements) {
+ this.updatePopulatedState(field);
+ this.updateCustomValidity(field);
+ }
+ }
+ }
+
+ setDefaultSelectedOptionByValue(select, value) {
+ for (let option of select.options) {
+ option.defaultSelected = option.value == value;
+ }
+ }
+
+ /**
+ * Get a record from the form suitable for a save/update in storage.
+ *
+ * @returns {object}
+ */
+ buildFormObject() {
+ let initialObject = {};
+ if (this.hasMailingAddressFields) {
+ // Start with an empty string for each mailing-address field so that any
+ // fields hidden for the current country are blanked in the return value.
+ initialObject = {
+ "street-address": "",
+ "address-level3": "",
+ "address-level2": "",
+ "address-level1": "",
+ "postal-code": "",
+ };
+ }
+
+ return Array.from(this._elements.form.elements).reduce((obj, input) => {
+ if (!input.disabled) {
+ obj[input.id] = input.value;
+ }
+ return obj;
+ }, initialObject);
+ }
+
+ /**
+ * Handle events
+ *
+ * @param {DOMEvent} event
+ */
+ handleEvent(event) {
+ switch (event.type) {
+ case "change": {
+ this.handleChange(event);
+ break;
+ }
+ case "input": {
+ this.handleInput(event);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle change events
+ *
+ * @param {DOMEvent} event
+ */
+ handleChange(event) {
+ this.updatePopulatedState(event.target);
+ }
+
+ /**
+ * Handle input events
+ */
+ handleInput(_e) {}
+
+ /**
+ * Attach event listener
+ */
+ attachEventListeners() {
+ this._elements.form.addEventListener("input", this);
+ }
+
+ /**
+ * Set the field-populated attribute if the field has a value.
+ *
+ * @param {DOMElement} field The field that will be checked for a value.
+ */
+ updatePopulatedState(field) {
+ let span = field.parentNode.querySelector(".label-text");
+ if (!span) {
+ return;
+ }
+ span.toggleAttribute("field-populated", !!field.value.trim());
+ }
+
+ /**
+ * Run custom validity routines specific to the field and type of form.
+ *
+ * @param {DOMElement} _field The field that will be validated.
+ */
+ updateCustomValidity(_field) {}
+}
+
+export class EditCreditCard extends EditAutofillForm {
+ /**
+ * @param {HTMLElement[]} elements
+ * @param {object} record with a decrypted cc-number
+ * @param {object} addresses in an object with guid keys for the billing address picker.
+ */
+ constructor(elements, record, addresses) {
+ super(elements);
+
+ this._addresses = addresses;
+ Object.assign(this._elements, {
+ ccNumber: this._elements.form.querySelector("#cc-number"),
+ invalidCardNumberStringElement: this._elements.form.querySelector(
+ "#invalidCardNumberString"
+ ),
+ month: this._elements.form.querySelector("#cc-exp-month"),
+ year: this._elements.form.querySelector("#cc-exp-year"),
+ billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
+ billingAddressRow:
+ this._elements.form.querySelector(".billingAddressRow"),
+ });
+
+ this.attachEventListeners();
+ this.loadRecord(record, addresses);
+ }
+
+ loadRecord(record, addresses, preserveFieldValues) {
+ // _record must be updated before generateYears and generateBillingAddressOptions are called.
+ this._record = record;
+ this._addresses = addresses;
+ this.generateBillingAddressOptions(preserveFieldValues);
+ if (!preserveFieldValues) {
+ // Re-generating the months will reset the selected option.
+ this.generateMonths();
+ // Re-generating the years will reset the selected option.
+ this.generateYears();
+ super.loadRecord(record);
+ }
+ }
+
+ generateMonths() {
+ const count = 12;
+
+ // Clear the list
+ this._elements.month.textContent = "";
+
+ // Empty month option
+ this._elements.month.appendChild(new Option());
+
+ // Populate month list. Format: "month number - month name"
+ let dateFormat = new Intl.DateTimeFormat(navigator.language, {
+ month: "long",
+ }).format;
+ for (let i = 0; i < count; i++) {
+ let monthNumber = (i + 1).toString();
+ let monthName = dateFormat(new Date(1970, i));
+ let option = new Option();
+ option.value = monthNumber;
+ // XXX: Bug 1446164 - Localize this string.
+ option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
+ this._elements.month.appendChild(option);
+ }
+ }
+
+ generateYears() {
+ const count = 11;
+ const currentYear = new Date().getFullYear();
+ const ccExpYear = this._record && this._record["cc-exp-year"];
+
+ // Clear the list
+ this._elements.year.textContent = "";
+
+ // Provide an empty year option
+ this._elements.year.appendChild(new Option());
+
+ if (ccExpYear && ccExpYear < currentYear) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+
+ for (let i = 0; i < count; i++) {
+ let year = currentYear + i;
+ let option = new Option(year);
+ this._elements.year.appendChild(option);
+ }
+
+ if (ccExpYear && ccExpYear > currentYear + count) {
+ this._elements.year.appendChild(new Option(ccExpYear));
+ }
+ }
+
+ generateBillingAddressOptions(preserveFieldValues) {
+ let billingAddressGUID;
+ if (preserveFieldValues && this._elements.billingAddress.value) {
+ billingAddressGUID = this._elements.billingAddress.value;
+ } else if (this._record) {
+ billingAddressGUID = this._record.billingAddressGUID;
+ }
+
+ this._elements.billingAddress.textContent = "";
+
+ this._elements.billingAddress.appendChild(new Option("", ""));
+
+ let hasAddresses = false;
+ for (let [guid, address] of Object.entries(this._addresses)) {
+ hasAddresses = true;
+ let selected = guid == billingAddressGUID;
+ let option = new Option(
+ lazy.FormAutofillUtils.getAddressLabel(address),
+ guid,
+ selected,
+ selected
+ );
+ this._elements.billingAddress.appendChild(option);
+ }
+
+ this._elements.billingAddressRow.hidden = !hasAddresses;
+ }
+
+ attachEventListeners() {
+ this._elements.form.addEventListener("change", this);
+ super.attachEventListeners();
+ }
+
+ handleInput(event) {
+ // Clear the error message if cc-number is valid
+ if (
+ event.target == this._elements.ccNumber &&
+ lazy.FormAutofillUtils.isCCNumber(this._elements.ccNumber.value)
+ ) {
+ this._elements.ccNumber.setCustomValidity("");
+ }
+ super.handleInput(event);
+ }
+
+ updateCustomValidity(field) {
+ super.updateCustomValidity(field);
+
+ // Mark the cc-number field as invalid if the number is empty or invalid.
+ if (
+ field == this._elements.ccNumber &&
+ !lazy.FormAutofillUtils.isCCNumber(field.value)
+ ) {
+ let invalidCardNumberString =
+ this._elements.invalidCardNumberStringElement.textContent;
+ field.setCustomValidity(invalidCardNumberString || " ");
+ }
+ }
+}
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/editAddress.xhtml b/browser/extensions/formautofill/content/editAddress.xhtml
index 47ae4a2a3b..a23fa5ab8c 100644
--- a/browser/extensions/formautofill/content/editAddress.xhtml
+++ b/browser/extensions/formautofill/content/editAddress.xhtml
@@ -19,65 +19,13 @@
rel="stylesheet"
href="chrome://formautofill/content/skin/editDialog.css"
/>
- <script src="chrome://formautofill/content/editDialog.js"></script>
- <script src="chrome://formautofill/content/autofillEditForms.js"></script>
<script
type="module"
src="chrome://global/content/elements/moz-button-group.mjs"
></script>
</head>
<body>
- <form id="form" class="editAddressForm" autocomplete="off">
- <!--
- The <span class="label-text" …/> needs to be after the form field in the same element in
- order to get proper label styling with :focus and :user-invalid
- -->
- <label id="name-container" class="container">
- <input id="name" type="text" required="required" />
- <span data-l10n-id="autofill-address-name" class="label-text" />
- </label>
- <label id="organization-container" class="container">
- <input id="organization" type="text" />
- <span data-l10n-id="autofill-address-organization" class="label-text" />
- </label>
- <label id="street-address-container" class="container">
- <textarea id="street-address" rows="3" />
- <span data-l10n-id="autofill-address-street" class="label-text" />
- </label>
- <label id="address-level3-container" class="container">
- <input id="address-level3" type="text" />
- <span class="label-text" />
- </label>
- <label id="address-level2-container" class="container">
- <input id="address-level2" type="text" />
- <span class="label-text" />
- </label>
- <label id="address-level1-container" class="container">
- <!-- The address-level1 input will get replaced by a select dropdown
- by autofillEditForms.js when the selected country has provided
- specific options. -->
- <input id="address-level1" type="text" />
- <span class="label-text" />
- </label>
- <label id="postal-code-container" class="container">
- <input id="postal-code" type="text" />
- <span class="label-text" />
- </label>
- <label id="country-container" class="container">
- <select id="country" required="required">
- <option />
- </select>
- <span data-l10n-id="autofill-address-country" class="label-text" />
- </label>
- <label id="tel-container" class="container">
- <input id="tel" type="tel" dir="auto" />
- <span data-l10n-id="autofill-address-tel" class="label-text" />
- </label>
- <label id="email-container" class="container">
- <input id="email" type="email" required="required" />
- <span data-l10n-id="autofill-address-email" class="label-text" />
- </label>
- </form>
+ <form id="form" class="editAddressForm" autocomplete="off"></form>
<div id="controls-container">
<span
id="country-warning-message"
@@ -88,31 +36,25 @@
<button id="save" class="primary" data-l10n-id="autofill-save-button" />
</moz-button-group>
</div>
- <script>
- <![CDATA[
- "use strict";
+ <!-- eslint-disable -->
+ <script type="module">
+ import { createFormLayoutFromRecord } from "chrome://formautofill/content/addressFormLayout.mjs";
+ import { EditAddressDialog } from "chrome://formautofill/content/editDialog.mjs";
- const {
- record,
- noValidate,
- } = window.arguments?.[0] ?? {};
+ const { record, noValidate, l10nStrings } = window.arguments?.[0] ?? {};
+ const formElement = document.querySelector("form");
+ formElement.noValidate = !!noValidate;
+ createFormLayoutFromRecord(formElement, record, l10nStrings);
- /* import-globals-from autofillEditForms.js */
- const fieldContainer = new EditAddress({
- form: document.getElementById("form"),
- }, record, {
- noValidate,
- });
-
- /* import-globals-from editDialog.js */
- new EditAddressDialog({
- title: document.querySelector("title"),
- fieldContainer,
- controlsContainer: document.getElementById("controls-container"),
- cancel: document.getElementById("cancel"),
- save: document.getElementById("save"),
- }, record);
- ]]>
+ new EditAddressDialog(
+ {
+ title: document.querySelector("title"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ },
+ record
+ );
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/editCreditCard.xhtml b/browser/extensions/formautofill/content/editCreditCard.xhtml
index 920be841c5..8fceb5709b 100644
--- a/browser/extensions/formautofill/content/editCreditCard.xhtml
+++ b/browser/extensions/formautofill/content/editCreditCard.xhtml
@@ -19,8 +19,6 @@
rel="stylesheet"
href="chrome://formautofill/content/skin/editDialog.css"
/>
- <script src="chrome://formautofill/content/editDialog.js"></script>
- <script src="chrome://formautofill/content/autofillEditForms.js"></script>
</head>
<body>
<form id="form" class="editCreditCardForm contentPane" autocomplete="off">
@@ -87,36 +85,32 @@
<button id="cancel" data-l10n-id="autofill-cancel-button" />
<button id="save" class="primary" data-l10n-id="autofill-save-button" />
</div>
- <script>
- <![CDATA[
- "use strict";
- /* import-globals-from editDialog.js */
+ <!-- eslint-disable -->
+ <script type="module">
+ import { EditCreditCardDialog } from "chrome://formautofill/content/editDialog.mjs";
+ import { EditCreditCard } from "chrome://formautofill/content/autofillEditForms.mjs";
+ const { record } = window.arguments?.[0] ?? {};
- (async () => {
- const {
- record,
- } = window.arguments?.[0] ?? {};
+ const fieldContainer = new EditCreditCard(
+ {
+ form: document.getElementById("form"),
+ },
+ record,
+ []
+ );
- const addresses = {};
- for (let address of await formAutofillStorage.addresses.getAll()) {
- addresses[address.guid] = address;
- }
-
- /* import-globals-from autofillEditForms.js */
- const fieldContainer = new EditCreditCard({
- form: document.getElementById("form"),
- }, record, addresses);
-
- new EditCreditCardDialog({
- title: document.querySelector("title"),
- fieldContainer,
- controlsContainer: document.getElementById("controls-container"),
- cancel: document.getElementById("cancel"),
- save: document.getElementById("save"),
- }, record);
- })();
- ]]>
+ new EditCreditCardDialog(
+ {
+ title: document.querySelector("title"),
+ fieldContainer,
+ controlsContainer: document.getElementById("controls-container"),
+ cancel: document.getElementById("cancel"),
+ save: document.getElementById("save"),
+ },
+ record
+ );
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/editDialog.js b/browser/extensions/formautofill/content/editDialog.mjs
index 467acbdd07..5371051e12 100644
--- a/browser/extensions/formautofill/content/editDialog.js
+++ b/browser/extensions/formautofill/content/editDialog.mjs
@@ -2,24 +2,27 @@
* 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/. */
-/* exported EditAddressDialog, EditCreditCardDialog */
/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.
-"use strict";
+import {
+ getCurrentFormData,
+ canSubmitForm,
+} from "chrome://formautofill/content/addressFormLayout.mjs";
-ChromeUtils.defineESModuleGetters(this, {
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
});
class AutofillEditDialog {
constructor(subStorageName, elements, record) {
- this._storageInitPromise = formAutofillStorage.initialize();
+ this._storageInitPromise = lazy.formAutofillStorage.initialize();
this._subStorageName = subStorageName;
this._elements = elements;
this._record = record;
this.localizeDocument();
- window.addEventListener("DOMContentLoaded", this, { once: true });
+ window.addEventListener("load", this, { once: true });
}
async init() {
@@ -28,7 +31,7 @@ class AutofillEditDialog {
// For testing only: signal to tests that the dialog is ready for testing.
// This is likely no longer needed since retrieving from storage is fully
// handled in manageDialog.js now.
- window.dispatchEvent(new CustomEvent("FormReady"));
+ window.dispatchEvent(new CustomEvent("FormReadyForTests"));
}
/**
@@ -38,7 +41,7 @@ class AutofillEditDialog {
*/
async getStorage() {
await this._storageInitPromise;
- return formAutofillStorage[this._subStorageName];
+ return lazy.formAutofillStorage[this._subStorageName];
}
/**
@@ -63,7 +66,7 @@ class AutofillEditDialog {
*/
handleEvent(event) {
switch (event.type) {
- case "DOMContentLoaded": {
+ case "load": {
this.init();
break;
}
@@ -139,7 +142,8 @@ class AutofillEditDialog {
attachEventListeners() {
window.addEventListener("keypress", this);
window.addEventListener("contextmenu", this);
- this._elements.controlsContainer.addEventListener("click", this);
+ this._elements.save.addEventListener("click", this);
+ this._elements.cancel.addEventListener("click", this);
document.addEventListener("input", this);
}
@@ -148,17 +152,20 @@ class AutofillEditDialog {
recordFormSubmit() {
let method = this._record?.guid ? "edit" : "add";
- AutofillTelemetry.recordManageEvent(this.telemetryType, method);
+ lazy.AutofillTelemetry.recordManageEvent(this.telemetryType, method);
}
}
-class EditAddressDialog extends AutofillEditDialog {
- telemetryType = AutofillTelemetry.ADDRESS;
+export class EditAddressDialog extends AutofillEditDialog {
+ telemetryType = lazy.AutofillTelemetry.ADDRESS;
constructor(elements, record) {
super("addresses", elements, record);
if (record) {
- AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
+ lazy.AutofillTelemetry.recordManageEvent(
+ this.telemetryType,
+ "show_entry"
+ );
}
}
@@ -171,9 +178,19 @@ class EditAddressDialog extends AutofillEditDialog {
}
}
+ updateSaveButtonState() {
+ // Toggle disabled attribute on the save button based on
+ // whether the form is filled or empty.
+ if (!canSubmitForm()) {
+ this._elements.save.setAttribute("disabled", true);
+ } else {
+ this._elements.save.removeAttribute("disabled");
+ }
+ }
+
async handleSubmit() {
await this.saveRecord(
- this._elements.fieldContainer.buildFormObject(),
+ getCurrentFormData(),
this._record ? this._record.guid : null
);
this.recordFormSubmit();
@@ -182,8 +199,8 @@ class EditAddressDialog extends AutofillEditDialog {
}
}
-class EditCreditCardDialog extends AutofillEditDialog {
- telemetryType = AutofillTelemetry.CREDIT_CARD;
+export class EditCreditCardDialog extends AutofillEditDialog {
+ telemetryType = lazy.AutofillTelemetry.CREDIT_CARD;
constructor(elements, record) {
elements.fieldContainer._elements.billingAddress.disabled = true;
@@ -193,7 +210,10 @@ class EditCreditCardDialog extends AutofillEditDialog {
this._onCCNumberFieldBlur.bind(this)
);
if (record) {
- AutofillTelemetry.recordManageEvent(this.telemetryType, "show_entry");
+ lazy.AutofillTelemetry.recordManageEvent(
+ this.telemetryType,
+ "show_entry"
+ );
}
}
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/content/manageAddresses.xhtml b/browser/extensions/formautofill/content/manageAddresses.xhtml
index 68e810179e..2c8f0608f7 100644
--- a/browser/extensions/formautofill/content/manageAddresses.xhtml
+++ b/browser/extensions/formautofill/content/manageAddresses.xhtml
@@ -16,7 +16,6 @@
rel="stylesheet"
href="chrome://formautofill/content/manageDialog.css"
/>
- <script src="chrome://formautofill/content/manageDialog.js"></script>
</head>
<body>
<fieldset>
@@ -39,9 +38,12 @@
data-l10n-id="autofill-manage-edit-button"
/>
</div>
- <script>
- "use strict";
- /* global ManageAddresses */
+ <!-- eslint-disable -->
+ <!-- For some reason eslint complains here about import only available for sourceType: "module" -->
+ <!-- even though type is set to module.-->
+ <script type="module">
+ import { ManageAddresses } from "chrome://formautofill/content/manageDialog.mjs";
+
new ManageAddresses({
records: document.getElementById("addresses"),
controlsContainer: document.getElementById("controls-container"),
@@ -50,5 +52,6 @@
edit: document.getElementById("edit"),
});
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/manageCreditCards.xhtml b/browser/extensions/formautofill/content/manageCreditCards.xhtml
index e7baf9d364..69aae82df9 100644
--- a/browser/extensions/formautofill/content/manageCreditCards.xhtml
+++ b/browser/extensions/formautofill/content/manageCreditCards.xhtml
@@ -18,7 +18,6 @@
rel="stylesheet"
href="chrome://formautofill/content/manageDialog.css"
/>
- <script src="chrome://formautofill/content/manageDialog.js"></script>
</head>
<body>
<fieldset>
@@ -41,9 +40,12 @@
data-l10n-id="autofill-manage-edit-button"
/>
</div>
- <script>
- "use strict";
- /* global ManageCreditCards */
+ <!-- eslint-disable -->
+ <!-- For some reason eslint complains here about import only available for sourceType: "module" -->
+ <!-- eventhough type is set to module -->
+ <script type="module">
+ import { ManageCreditCards } from "chrome://formautofill/content/manageDialog.mjs";
+
new ManageCreditCards({
records: document.getElementById("credit-cards"),
controlsContainer: document.getElementById("controls-container"),
@@ -52,5 +54,6 @@
edit: document.getElementById("edit"),
});
</script>
+ <!-- eslint-enable -->
</body>
</html>
diff --git a/browser/extensions/formautofill/content/manageDialog.js b/browser/extensions/formautofill/content/manageDialog.mjs
index ad5cefbb15..bca0f48f40 100644
--- a/browser/extensions/formautofill/content/manageDialog.js
+++ b/browser/extensions/formautofill/content/manageDialog.mjs
@@ -2,10 +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/. */
-/* exported ManageAddresses, ManageCreditCards */
-
-"use strict";
-
const EDIT_ADDRESS_URL = "chrome://formautofill/content/editAddress.xhtml";
const EDIT_CREDIT_CARD_URL =
"chrome://formautofill/content/editCreditCard.xhtml";
@@ -20,38 +16,44 @@ const { AutofillTelemetry } = ChromeUtils.importESModule(
"resource://gre/modules/shared/AutofillTelemetry.sys.mjs"
);
-ChromeUtils.defineESModuleGetters(this, {
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
CreditCard: "resource://gre/modules/CreditCard.sys.mjs",
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
formAutofillStorage: "resource://autofill/FormAutofillStorage.sys.mjs",
});
-this.log = null;
-ChromeUtils.defineLazyGetter(this, "log", () =>
- FormAutofill.defineLogGetter(this, "manageAddresses")
+ChromeUtils.defineLazyGetter(lazy, "log", () =>
+ FormAutofill.defineLogGetter(lazy, "manageAddresses")
+);
+
+ChromeUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () => new Localization([" browser/preferences/formAutofill.ftl"], true)
);
class ManageRecords {
constructor(subStorageName, elements) {
- this._storageInitPromise = formAutofillStorage.initialize();
+ this._storageInitPromise = lazy.formAutofillStorage.initialize();
this._subStorageName = subStorageName;
this._elements = elements;
this._newRequest = false;
this._isLoadingRecords = false;
this.prefWin = window.opener;
- window.addEventListener("DOMContentLoaded", this, { once: true });
+ window.addEventListener("load", this, { once: true });
}
async init() {
await this.loadRecords();
this.attachEventListeners();
// For testing only: Notify when the dialog is ready for interaction
- window.dispatchEvent(new CustomEvent("FormReady"));
+ window.dispatchEvent(new CustomEvent("FormReadyForTests"));
}
uninit() {
- log.debug("uninit");
+ lazy.log.debug("uninit");
this.detachEventListeners();
this._elements = null;
}
@@ -72,7 +74,7 @@ class ManageRecords {
*/
async getStorage() {
await this._storageInitPromise;
- return formAutofillStorage[this._subStorageName];
+ return lazy.formAutofillStorage[this._subStorageName];
}
/**
@@ -146,9 +148,9 @@ class ManageRecords {
* Remove all existing record elements.
*/
clearRecordElements() {
- let parent = this._elements.records;
- while (parent.lastChild) {
- parent.removeChild(parent.lastChild);
+ const parentElement = this._elements.records;
+ while (parentElement.lastChild) {
+ parentElement.removeChild(parentElement.lastChild);
}
}
@@ -186,7 +188,7 @@ class ManageRecords {
* @param {number} selectedCount
*/
updateButtonsStates(selectedCount) {
- log.debug("updateButtonsStates:", selectedCount);
+ lazy.log.debug("updateButtonsStates:", selectedCount);
if (selectedCount == 0) {
this._elements.edit.setAttribute("disabled", "disabled");
this._elements.remove.setAttribute("disabled", "disabled");
@@ -209,7 +211,7 @@ class ManageRecords {
*/
handleEvent(event) {
switch (event.type) {
- case "DOMContentLoaded": {
+ case "load": {
this.init();
break;
}
@@ -302,18 +304,33 @@ class ManageRecords {
}
}
-class ManageAddresses extends ManageRecords {
+export class ManageAddresses extends ManageRecords {
telemetryType = AutofillTelemetry.ADDRESS;
constructor(elements) {
super("addresses", elements);
elements.add.setAttribute(
"search-l10n-ids",
- FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
+ lazy.FormAutofillUtils.EDIT_ADDRESS_L10N_IDS.join(",")
);
AutofillTelemetry.recordManageEvent(this.telemetryType, "show");
}
+ static getAddressL10nStrings() {
+ const l10nIds = [
+ ...lazy.FormAutofillUtils.MANAGE_ADDRESSES_L10N_IDS,
+ ...lazy.FormAutofillUtils.EDIT_ADDRESS_L10N_IDS,
+ ];
+
+ return l10nIds.reduce(
+ (acc, id) => ({
+ ...acc,
+ [id]: lazy.l10n.formatValueSync(id),
+ }),
+ {}
+ );
+ }
+
/**
* Open the edit address dialog to create/edit an address.
*
@@ -325,22 +342,23 @@ class ManageAddresses extends ManageRecords {
// Don't validate in preferences since it's fine for fields to be missing
// for autofill purposes. For PaymentRequest addresses get more validation.
noValidate: true,
+ l10nStrings: ManageAddresses.getAddressL10nStrings(),
});
}
getLabelInfo(address) {
- return { raw: FormAutofillUtils.getAddressLabel(address) };
+ return { raw: lazy.FormAutofillUtils.getAddressLabel(address) };
}
}
-class ManageCreditCards extends ManageRecords {
+export class ManageCreditCards extends ManageRecords {
telemetryType = AutofillTelemetry.CREDIT_CARD;
constructor(elements) {
super("creditCards", elements);
elements.add.setAttribute(
"search-l10n-ids",
- FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
+ lazy.FormAutofillUtils.EDIT_CREDITCARD_L10N_IDS.join(",")
);
this._isDecrypted = false;
@@ -355,22 +373,24 @@ class ManageCreditCards extends ManageRecords {
async openEditDialog(creditCard) {
// Ask for reauth if user is trying to edit an existing credit card.
if (creditCard) {
- const promptMessage = FormAutofillUtils.reauthOSPromptMessage(
+ const promptMessage = lazy.FormAutofillUtils.reauthOSPromptMessage(
"autofill-edit-payment-method-os-prompt-macos",
"autofill-edit-payment-method-os-prompt-windows",
"autofill-edit-payment-method-os-prompt-other"
);
- const loggedIn = await FormAutofillUtils.ensureLoggedIn(promptMessage);
- if (!loggedIn.authenticated) {
+ const verified = await lazy.FormAutofillUtils.verifyUserOSAuth(
+ FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ promptMessage
+ );
+ if (!verified) {
return;
}
}
-
let decryptedCCNumObj = {};
if (creditCard && creditCard["cc-number-encrypted"]) {
try {
- decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(
+ decryptedCCNumObj["cc-number"] = await lazy.OSKeyStore.decrypt(
creditCard["cc-number-encrypted"]
);
} catch (ex) {
@@ -410,11 +430,11 @@ class ManageCreditCards extends ManageRecords {
// Since the text content is generated by Fluent, aria-label must be
// generated by Fluent also.
const type = creditCard["cc-type"];
- const typeL10nId = CreditCard.getNetworkL10nId(type);
+ const typeL10nId = lazy.CreditCard.getNetworkL10nId(type);
const typeName = typeL10nId
? await document.l10n.formatValue(typeL10nId)
: type ?? ""; // Unknown card type
- return CreditCard.getLabelInfo({
+ return lazy.CreditCard.getLabelInfo({
name: creditCard["cc-name"],
number: creditCard["cc-number"],
month: creditCard["cc-exp-month"],
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/shared/editAddress.css b/browser/extensions/formautofill/skin/shared/editAddress.css
index c50024e542..7660fd8e55 100644
--- a/browser/extensions/formautofill/skin/shared/editAddress.css
+++ b/browser/extensions/formautofill/skin/shared/editAddress.css
@@ -18,6 +18,15 @@ dialog:not([subdialog]) .editAddressForm {
margin-top: var(--grid-column-row-gap) !important;
margin-inline: calc(var(--grid-column-row-gap) / 2);
flex-grow: 1;
+
+ &.new-line {
+ flex: 0 1 100%;
+ }
+
+ input, textarea, select {
+ width: 100%;
+ margin: 0;
+ }
}
#country-container {
@@ -29,12 +38,6 @@ dialog:not([subdialog]) .editAddressForm {
max-width: calc(50% - var(--grid-column-row-gap));
}
-#name-container,
-#street-address-container {
- /* Name and street address are always full-width */
- flex: 0 1 100%;
-}
-
#street-address {
resize: vertical;
}
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.toml b/browser/extensions/formautofill/test/browser/address/browser.toml
index 8b7f1ec760..e8c72ae1b1 100644
--- a/browser/extensions/formautofill/test/browser/address/browser.toml
+++ b/browser/extensions/formautofill/test/browser/address/browser.toml
@@ -38,6 +38,8 @@ support-files = [
["browser_address_doorhanger_ui.js"]
+["browser_address_doorhanger_ui_lines.js"]
+
["browser_address_doorhanger_unsupported_region.js"]
["browser_address_telemetry.js"]
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_address_doorhanger_ui_lines.js b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui_lines.js
new file mode 100644
index 0000000000..01e888a5f8
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/address/browser_address_doorhanger_ui_lines.js
@@ -0,0 +1,32 @@
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.formautofill.addresses.capture.enabled", true],
+ ["extensions.formautofill.addresses.supported", "on"],
+ ],
+ });
+});
+
+add_task(
+ async function test_address_line_displays_normalized_state_in_save_doorhanger() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: ADDRESS_FORM_URL },
+ async function (browser) {
+ await showAddressDoorhanger(browser, {
+ "#address-level1": "Nova Scotia",
+ "#address-level2": "Somerset",
+ "#country": "CA",
+ });
+
+ const p = getNotification().querySelector(
+ `.address-save-update-row-container p:first-child`
+ );
+ is(p.textContent, "Somerset, NS");
+
+ await clickAddressDoorhangerButton(SECONDARY_BUTTON);
+ }
+ );
+ }
+);
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/browser_editAddressDialog.js b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
index 62797739fc..dec367d8e9 100644
--- a/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
+++ b/browser/extensions/formautofill/test/browser/browser_editAddressDialog.js
@@ -1,7 +1,7 @@
"use strict";
-const { FormAutofillUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+const { FormAutofill } = ChromeUtils.importESModule(
+ "resource://autofill/FormAutofill.sys.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
@@ -53,7 +53,12 @@ add_task(async function test_defaultCountry() {
Region._setHomeRegion("XX", false);
await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
let doc = win.document;
- is(doc.querySelector("#country").value, "", "Default country set to empty");
+ const countries = [...FormAutofill.countries.keys()];
+ is(
+ countries[0],
+ doc.querySelector("#country").value,
+ "Default country set to first option in the list"
+ );
doc.querySelector("#cancel").click();
});
Region._setHomeRegion("US", false);
@@ -250,10 +255,9 @@ add_task(async function test_saveAddressCA() {
"Postal Code",
"CA postal-code label should be 'Postal Code'"
);
- is(
- doc.querySelector("#address-level3-container").style.display,
- "none",
- "CA address-level3 should be hidden"
+ ok(
+ !doc.querySelector("#address-level3-container"),
+ "CA address-level3 should not be rendered"
);
// Input address info and verify move through form with tab keys
@@ -313,15 +317,13 @@ add_task(async function test_saveAddressDE() {
"Postal Code",
"DE postal-code label should be 'Postal Code'"
);
- is(
- doc.querySelector("#address-level1-container").style.display,
- "none",
- "DE address-level1 should be hidden"
+ ok(
+ !doc.querySelector("#address-level1-container"),
+ "DE address-level1 should not be rendered"
);
- is(
- doc.querySelector("#address-level3-container").style.display,
- "none",
- "DE address-level3 should be hidden"
+ ok(
+ !doc.querySelector("#address-level3-container"),
+ "DE address-level3 should not be rendered"
);
// Input address info and verify move through form with tab keys
doc.querySelector("#name").focus();
@@ -434,57 +436,28 @@ add_task(async function test_saveAddressIE() {
add_task(async function test_countryAndStateFieldLabels() {
await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
- let doc = win.document;
- // Change country to verify labels
- doc.querySelector("#country").focus();
-
- let mutableLabels = [
- "postal-code-container",
- "address-level1-container",
- "address-level2-container",
- "address-level3-container",
- ].map(containerID =>
- doc.getElementById(containerID).querySelector(":scope > .label-text")
- );
-
+ const doc = win.document;
for (let countryOption of doc.querySelector("#country").options) {
- if (countryOption.value == "") {
- info("Skipping the empty country option");
- continue;
- }
-
// Clear L10N textContent to not leave leftovers between country tests
- for (let labelEl of mutableLabels) {
+ for (const labelEl of doc.querySelectorAll(".label-text")) {
doc.l10n.setAttributes(labelEl, "");
labelEl.textContent = "";
}
+ // Change country to verify labels
+ doc.querySelector("#country").focus();
+
info(`Selecting '${countryOption.label}' (${countryOption.value})`);
EventUtils.synthesizeKey(countryOption.label, {}, win);
- let l10nResolve;
- let l10nReady = new Promise(resolve => {
- l10nResolve = resolve;
- });
- let verifyL10n = () => {
- if (mutableLabels.every(labelEl => labelEl.textContent)) {
- win.removeEventListener("MozAfterPaint", verifyL10n);
- l10nResolve();
- }
- };
- win.addEventListener("MozAfterPaint", verifyL10n);
- await l10nReady;
-
- // Check that the labels were filled
- for (let labelEl of mutableLabels) {
- isnot(
- labelEl.textContent,
- "",
- "Ensure textContent is non-empty for: " + countryOption.value
- );
- }
+ await waitForFocusAndFormReady(win);
+
+ const allLabelsHaveText = [...doc.querySelectorAll(".label-text")].every(
+ labelEl => labelEl.textContent
+ );
+
+ ok(allLabelsHaveText, "All labels are rendered and have text content");
- let stateOptions = doc.querySelector("#address-level1").options;
/* eslint-disable max-len */
let expectedStateOptions = {
BS: {
@@ -510,22 +483,22 @@ add_task(async function test_countryAndStateFieldLabels() {
/* eslint-enable max-len */
if (expectedStateOptions[countryOption.value]) {
+ const stateOptions = doc.querySelector("#address-level1").options;
let { keys, names } = expectedStateOptions[countryOption.value];
is(
stateOptions.length,
- keys.length + 1,
- "stateOptions should list all options plus a blank entry"
+ keys.length,
+ "stateOptions should have the same length as the expected options"
);
- is(stateOptions[0].value, "", "First State option should be blank");
for (let i = 1; i < stateOptions.length; i++) {
is(
stateOptions[i].value,
- keys[i - 1],
+ keys[i],
"Each State should be listed in alphabetical name order (key)"
);
is(
stateOptions[i].text,
- names[i - 1],
+ names[i],
"Each State should be listed in alphabetical name order (name)"
);
}
@@ -539,14 +512,15 @@ add_task(async function test_countryAndStateFieldLabels() {
});
add_task(async function test_hiddenFieldNotSaved() {
- await testDialog(EDIT_ADDRESS_DIALOG_URL, win => {
- let doc = win.document;
+ await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
+ const doc = win.document;
doc.querySelector("#address-level2").focus();
EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level2"], {}, win);
doc.querySelector("#address-level1").focus();
EventUtils.synthesizeKey(TEST_ADDRESS_1["address-level1"], {}, win);
doc.querySelector("#country").focus();
EventUtils.synthesizeKey("Germany", {}, win);
+ await waitForFocusAndFormReady(win);
doc.querySelector("#save").focus();
EventUtils.synthesizeKey("VK_RETURN", {}, win);
});
@@ -598,10 +572,11 @@ add_task(async function test_hiddenFieldRemovedWhenCountryChanged() {
await testDialog(
EDIT_ADDRESS_DIALOG_URL,
- win => {
- let doc = win.document;
+ async win => {
+ const doc = win.document;
doc.querySelector("#country").focus();
EventUtils.synthesizeKey("Germany", {}, win);
+ await waitForFocusAndFormReady(win);
win.document.querySelector("#save").click();
},
{
@@ -628,63 +603,33 @@ add_task(async function test_hiddenFieldRemovedWhenCountryChanged() {
add_task(async function test_countrySpecificFieldsGetRequiredness() {
Region._setHomeRegion("RO", false);
await testDialog(EDIT_ADDRESS_DIALOG_URL, async win => {
- let doc = win.document;
+ const doc = win.document;
is(
doc.querySelector("#country").value,
"RO",
"Default country set to Romania"
);
let provinceField = doc.getElementById("address-level1");
- ok(
- !provinceField.required,
- "address-level1 should not be marked as required"
- );
- ok(provinceField.disabled, "address-level1 should be marked as disabled");
- is(
- provinceField.parentNode.style.display,
- "none",
- "address-level1 is hidden for Romania"
- );
+ ok(!provinceField, "address-level1 should not be rendered");
doc.querySelector("#country").focus();
EventUtils.synthesizeKey("United States", {}, win);
+ await waitForFocusAndFormReady(win);
+ const stateField = doc.getElementById("address-level1");
- await TestUtils.waitForCondition(
- () => {
- provinceField = doc.getElementById("address-level1");
- return provinceField.parentNode.style.display != "none";
- },
- "Wait for address-level1 to become visible",
- 10
- );
-
- ok(provinceField.required, "address-level1 should be marked as required");
- ok(
- !provinceField.disabled,
- "address-level1 should not be marked as disabled"
- );
+ ok(stateField.required, "address-level1 should be marked as required");
+ ok(!stateField.disabled, "address-level1 should not be marked as disabled");
// Dispatch a dummy key event so that <select>'s incremental search is cleared.
EventUtils.synthesizeKey("VK_ACCEPT", {}, win);
-
doc.querySelector("#country").focus();
EventUtils.synthesizeKey("Romania", {}, win);
- await TestUtils.waitForCondition(
- () => {
- provinceField = doc.getElementById("address-level1");
- return provinceField.parentNode.style.display == "none";
- },
- "Wait for address-level1 to become hidden",
- 10
- );
-
+ await waitForFocusAndFormReady(win);
ok(
- provinceField.required,
- "address-level1 will still be marked as required"
+ !doc.getElementById("address-level1"),
+ "address-level1 is not rendered "
);
- ok(provinceField.disabled, "address-level1 should be marked as disabled");
-
doc.querySelector("#cancel").click();
});
});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser.toml b/browser/extensions/formautofill/test/browser/creditCard/browser.toml
index 580ce936d4..ead527488e 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser.toml
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser.toml
@@ -1,7 +1,6 @@
[DEFAULT]
prefs = [
"extensions.formautofill.creditCards.enabled=true",
- "extensions.formautofill.reauth.enabled=true",
"toolkit.telemetry.ipcBatchTimeout=0", # lower the interval for event telemetry in the content process to update the parent process
]
support-files = [
@@ -41,11 +40,13 @@ skip-if = [
]
["browser_creditCard_doorhanger_display.js"]
-skip-if = [
- "apple_catalina && !debug", # perma-fail see Bug 1655601
- "apple_silicon && !debug", # perma-fail see Bug 1655601
- "win11_2009 && ccov", # Bug 1655600
-]
+skip-if = ["true"] # Bug 1895422
+# Bug 1895422 - Fix this test for linux then uncomment.
+# skip-if = [
+# "apple_catalina && !debug", # perma-fail see Bug 1655601
+# "apple_silicon && !debug", # perma-fail see Bug 1655601
+# "win11_2009 && ccov", # Bug 1655600
+# ]
["browser_creditCard_doorhanger_fields.js"]
skip-if = [
@@ -102,6 +103,9 @@ skip-if = ["apple_silicon && !debug"] # Bug 1714221
["browser_creditCard_heuristics_cc_type.js"]
skip-if = ["apple_silicon && !debug"] # Bug 1714221
+["browser_creditCard_osAuth.js"]
+skip-if = ["os == 'linux'"]
+
["browser_creditCard_submission_autodetect_type.js"]
skip-if = ["apple_silicon && !debug"]
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js b/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js
index f7fc731e54..0398c6242d 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js
@@ -16,6 +16,26 @@ add_task(async function setup_storage() {
);
});
+async function disableOSAuthForThisTest() {
+ // Revert head.js change that mocks os auth
+ sinon.restore();
+
+ let oldValue = FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ );
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ false
+ );
+
+ registerCleanupFunction(() => {
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ oldValue
+ );
+ });
+}
+
add_task(async function test_active_delay() {
// This is a workaround for the fact that we don't have a way
// to know when the popup was opened exactly and this makes our test
@@ -26,11 +46,11 @@ add_task(async function test_active_delay() {
// gets opened and listen for it in this test before we check if the item
// is disabled.
await SpecialPowers.pushPrefEnv({
- set: [
- ["security.notification_enable_delay", 1000],
- ["extensions.formautofill.reauth.enabled", false],
- ],
+ set: [["security.notification_enable_delay", 1000]],
});
+
+ await disableOSAuthForThisTest();
+
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CC_URL },
async function (browser) {
@@ -86,10 +106,7 @@ add_task(async function test_active_delay() {
add_task(async function test_no_delay() {
await SpecialPowers.pushPrefEnv({
- set: [
- ["security.notification_enable_delay", 1000],
- ["extensions.formautofill.reauth.enabled", false],
- ],
+ set: [["security.notification_enable_delay", 1000]],
});
await BrowserTestUtils.withNewTab(
{ gBrowser, url: ADDRESS_URL },
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js
index 82122925d7..b5a8019f0d 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js
@@ -132,12 +132,14 @@ add_task(async function test_update_doorhanger_click_save() {
await setStorage(TEST_CREDIT_CARD_1);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onChanged = waitForStorageChangedEvents("add");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let onPopupShown = waitForPopupShown();
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
@@ -153,7 +155,10 @@ add_task(async function test_update_doorhanger_click_save() {
await onPopupShown;
await clickDoorhangerButton(SECONDARY_BUTTON);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ ok(osKeyStoreLoginShown, "OS re-auth promise Complete");
+ }
}
);
await onChanged;
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js
index 715eceb3eb..8db32d9462 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js
@@ -107,16 +107,20 @@ add_task(async function test_doorhanger_not_shown_when_autofill_untouched() {
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onUsed = waitForStorageChangedEvents("notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
await waitForAutofill(browser, "#cc-name", "John Doe");
await SpecialPowers.spawn(browser, [], async function () {
@@ -186,12 +190,15 @@ add_task(
await setStorage(TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2);
let creditCards = await getCreditCards();
is(creditCards.length, 2, "2 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onUsed = waitForStorageChangedEvents("notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await openPopupOn(browser, "form #cc-number");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
@@ -214,7 +221,9 @@ add_task(
await sleep(1000);
is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
}
);
await onUsed;
@@ -242,12 +251,15 @@ add_task(
let creditCards = await getCreditCards();
is(creditCards.length, 2, "2 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onUsed = waitForStorageChangedEvents("notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
await openPopupOn(browser, "form #cc-number");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
@@ -267,7 +279,9 @@ add_task(
await sleep(1000);
is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
}
);
await onUsed;
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js
index c1ebef737e..7ba8bfab91 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js
@@ -13,18 +13,23 @@ add_task(async function test_update_autofill_name_field() {
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onChanged = waitForStorageChangedEvents("update", "notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let onPopupShown = waitForPopupShown();
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ ok(osKeyStoreLoginShown, "OS Auth Dialog shown and authenticated");
+ }
await waitForAutofill(browser, "#cc-name", "John Doe");
await focusUpdateSubmitForm(browser, {
@@ -63,17 +68,22 @@ add_task(async function test_update_autofill_exp_date_field() {
await setStorage(TEST_CREDIT_CARD_1);
let creditCards = await getCreditCards();
is(creditCards.length, 1, "1 credit card in storage");
+ let osKeyStoreLoginShown = null;
let onChanged = waitForStorageChangedEvents("update", "notifyUsed");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let onPopupShown = waitForPopupShown();
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ ok(osKeyStoreLoginShown, "OS Auth Dialog shown and authenticated");
+ }
await waitForAutofill(browser, "#cc-name", "John Doe");
await focusUpdateSubmitForm(browser, {
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js
index 2781e5acf6..774c3d7b25 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js
@@ -30,8 +30,10 @@ add_task(async function test_iframe_submit_untouched_creditCard_form() {
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_IFRAME_URL },
async function (browser) {
- let osKeyStoreLoginShown =
- OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let iframeBC = browser.browsingContext.children[0];
await openPopupOnSubframe(browser, iframeBC, "form #cc-name");
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_creditCard_osAuth.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_osAuth.js
new file mode 100644
index 0000000000..0fe6e1e07c
--- /dev/null
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_osAuth.js
@@ -0,0 +1,200 @@
+"use strict";
+
+const PAGE_PREFS = "about:preferences";
+const PAGE_PRIVACY = PAGE_PREFS + "#privacy";
+const SELECTORS = {
+ savedCreditCardsBtn: "#creditCardAutofill button",
+ reauthCheckbox: "#creditCardReauthenticate checkbox",
+};
+
+// On mac, this test times out in chaos mode
+requestLongerTimeout(2);
+
+add_setup(async function () {
+ // Revert head.js change that mocks os auth
+ sinon.restore();
+
+ // Load in a few credit cards
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.reduceTimerPrecision", false]],
+ });
+ await setStorage(TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2);
+});
+
+add_task(async function test_os_auth_enabled_with_checkbox() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+
+ await SpecialPowers.spawn(
+ browser,
+ [SELECTORS, AppConstants.NIGHTLY_BUILD],
+ async (selectors, isNightly) => {
+ is(
+ content.document.querySelector(selectors.reauthCheckbox).checked,
+ isNightly,
+ "OSReauth for credit cards should be checked"
+ );
+ }
+ );
+ is(
+ FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ ),
+ AppConstants.NIGHTLY_BUILD,
+ "OSAuth should be enabled."
+ );
+ }
+ );
+});
+
+add_task(async function test_os_auth_disabled_with_checkbox() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ false
+ );
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+
+ await SpecialPowers.spawn(browser, [SELECTORS], async selectors => {
+ is(
+ content.document.querySelector(selectors.reauthCheckbox).checked,
+ false,
+ "OSReauth for credit cards should be unchecked"
+ );
+ });
+ is(
+ FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ ),
+ false,
+ "OSAuth should be disabled"
+ );
+ }
+ );
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ true
+ );
+});
+
+add_task(async function test_OSAuth_enabled_with_random_value_in_pref() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF, "poutine-gravy"],
+ ],
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(browser, [SELECTORS], async selectors => {
+ let reauthCheckbox = content.document.querySelector(
+ selectors.reauthCheckbox
+ );
+ is(
+ reauthCheckbox.checked,
+ true,
+ "OSReauth for credit cards should be checked"
+ );
+ });
+ is(
+ FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ ),
+ true,
+ "OSAuth should be enabled since the pref does not decrypt to 'opt out'."
+ );
+ }
+ );
+});
+
+add_task(async function test_osAuth_enabled_behaviour() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ await SpecialPowers.pushPrefEnv({
+ set: [[FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF, ""]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
+ // The rest of the test uses Edit mode which causes an OS prompt in official builds.
+ return;
+ }
+ let reauthObserved = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ await SpecialPowers.spawn(browser, [SELECTORS], async selectors => {
+ content.document.querySelector(selectors.savedCreditCardsBtn).click();
+ });
+ let ccManageDialog = await waitForSubDialogLoad(
+ content,
+ MANAGE_CREDIT_CARDS_DIALOG_URL
+ );
+ await SpecialPowers.spawn(ccManageDialog, [], async () => {
+ let selRecords = content.document.getElementById("credit-cards");
+ await EventUtils.synthesizeMouseAtCenter(
+ selRecords.children[0],
+ [],
+ content
+ );
+ content.document.querySelector("#edit").click();
+ });
+ await reauthObserved; // If the OS does not popup, this will cause a timeout in the test.
+ await waitForSubDialogLoad(content, EDIT_CREDIT_CARD_DIALOG_URL);
+ }
+ );
+});
+
+add_task(async function test_osAuth_disabled_behavior() {
+ let finalPrefPaneLoaded = TestUtils.topicObserved("sync-pane-loaded");
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ false
+ );
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_PRIVACY },
+ async function (browser) {
+ await finalPrefPaneLoaded;
+ await SpecialPowers.spawn(
+ browser,
+ [SELECTORS.savedCreditCardsBtn, SELECTORS.reauthCheckbox],
+ async (saveButton, reauthCheckbox) => {
+ is(
+ content.document.querySelector(reauthCheckbox).checked,
+ false,
+ "OSReauth for credit cards should NOT be checked"
+ );
+ content.document.querySelector(saveButton).click();
+ }
+ );
+ let ccManageDialog = await waitForSubDialogLoad(
+ content,
+ MANAGE_CREDIT_CARDS_DIALOG_URL
+ );
+ await SpecialPowers.spawn(ccManageDialog, [], async () => {
+ let selRecords = content.document.getElementById("credit-cards");
+ await EventUtils.synthesizeMouseAtCenter(
+ selRecords.children[0],
+ [],
+ content
+ );
+ content.document.getElementById("edit").click();
+ });
+ info("The OS Auth dialog should NOT show up");
+ // If OSAuth prompt shows up, the next line would cause a timeout since the edit dialog would not show up.
+ await waitForSubDialogLoad(content, EDIT_CREDIT_CARD_DIALOG_URL);
+ }
+ );
+ FormAutofillUtils.setOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF,
+ true
+ );
+});
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
index 7a4bff1e45..ea455df12a 100644
--- a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
+++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js
@@ -154,12 +154,15 @@ async function openTabAndUseCreditCard(
creditCard,
{ closeTab = true, submitForm = true } = {}
) {
- let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = null;
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
CREDITCARD_FORM_URL
);
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let browser = tab.linkedBrowser;
await openPopupOn(browser, "form #cc-name");
@@ -167,7 +170,9 @@ async function openTabAndUseCreditCard(
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
}
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
await waitForAutofill(browser, "#cc-number", creditCard["cc-number"]);
await focusUpdateSubmitForm(
browser,
@@ -692,10 +697,14 @@ add_task(async function test_submit_creditCard_update() {
let creditCards = await getCreditCards();
Assert.equal(creditCards.length, 1, "1 credit card in storage");
- let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = null;
await BrowserTestUtils.withNewTab(
{ gBrowser, url: CREDITCARD_FORM_URL },
async function (browser) {
+ if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown =
+ OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
+ }
let onPopupShown = waitForPopupShown();
let onChanged;
if (expectChanged !== undefined) {
@@ -705,7 +714,9 @@ add_task(async function test_submit_creditCard_update() {
await openPopupOn(browser, "form #cc-name");
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser);
await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser);
- await osKeyStoreLoginShown;
+ if (osKeyStoreLoginShown) {
+ await osKeyStoreLoginShown;
+ }
await waitForAutofill(browser, "#cc-name", "John Doe");
await focusUpdateSubmitForm(browser, {
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..d82ed5076e 100644
--- a/browser/extensions/formautofill/test/browser/head.js
+++ b/browser/extensions/formautofill/test/browser/head.js
@@ -1,5 +1,9 @@
"use strict";
+const { ManageAddresses } = ChromeUtils.importESModule(
+ "chrome://formautofill/content/manageDialog.mjs"
+);
+
const { OSKeyStore } = ChromeUtils.importESModule(
"resource://gre/modules/OSKeyStore.sys.mjs"
);
@@ -20,6 +24,27 @@ const { FormAutofillNameUtils } = ChromeUtils.importESModule(
"resource://gre/modules/shared/FormAutofillNameUtils.sys.mjs"
);
+const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+);
+
+let { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+// Always pretend OS Auth is enabled in this dir.
+if (
+ gTestPath.includes("browser/creditCard") &&
+ OSKeyStoreTestUtils.canTestOSKeyStoreLogin() &&
+ OSKeyStore.canReauth()
+) {
+ info("Stubbing out getOSAuthEnabled so it always returns true");
+ sinon.stub(FormAutofillUtils, "getOSAuthEnabled").returns(true);
+ registerCleanupFunction(() => {
+ sinon.restore();
+ });
+}
+
const MANAGE_ADDRESSES_DIALOG_URL =
"chrome://formautofill/content/manageAddresses.xhtml";
const MANAGE_CREDIT_CARDS_DIALOG_URL =
@@ -535,25 +560,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 +601,7 @@ function waitPopupStateInChild(bc, messageName) {
async function openPopupOn(browser, selector) {
let childNotifiedPromise = waitPopupStateInChild(
browser,
- "FormAutoComplete:PopupOpened"
+ "AutoComplete:PopupOpened"
);
await SimpleTest.promiseFocus(browser);
@@ -613,7 +619,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 +643,7 @@ async function closePopup(browser) {
let childNotifiedPromise = waitPopupStateInChild(
browser,
- "FormAutoComplete:PopupClosed"
+ "AutoComplete:PopupClosed"
);
let popupClosePromise = BrowserTestUtils.waitForPopupEvent(
browser.autoCompletePopup,
@@ -655,7 +661,7 @@ async function closePopup(browser) {
async function closePopupForSubframe(browser, frameBrowsingContext) {
let childNotifiedPromise = waitPopupStateInChild(
browser,
- "FormAutoComplete:PopupClosed"
+ "AutoComplete:PopupClosed"
);
let popupClosePromise = BrowserTestUtils.waitForPopupEvent(
@@ -841,7 +847,7 @@ async function removeAllRecords() {
async function waitForFocusAndFormReady(win) {
return Promise.all([
new Promise(resolve => waitForFocus(resolve, win)),
- BrowserTestUtils.waitForEvent(win, "FormReady"),
+ BrowserTestUtils.waitForEvent(win, "FormReadyForTests"),
]);
}
@@ -850,14 +856,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,
@@ -880,9 +880,12 @@ async function testDialog(url, testFn, arg = undefined) {
"cc-number": await OSKeyStore.decrypt(arg.record["cc-number-encrypted"]),
});
}
- let win = window.openDialog(url, null, "width=600,height=600", arg);
+ const win = window.openDialog(url, null, "width=600,height=600", {
+ ...arg,
+ l10nStrings: ManageAddresses.getAddressL10nStrings(),
+ });
await waitForFocusAndFormReady(win);
- let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
+ const unloadPromise = BrowserTestUtils.waitForEvent(win, "unload");
await testFn(win);
return unloadPromise;
}
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml b/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml
index ffd504bb45..0d6ea02569 100644
--- a/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/mochitest.toml
@@ -2,7 +2,6 @@
prefs = [
"extensions.formautofill.creditCards.supported=on",
"extensions.formautofill.creditCards.enabled=true",
- "extensions.formautofill.reauth.enabled=true",
]
support-files = [
"!/toolkit/components/satchel/test/satchel_common.js",
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..9fad869629 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,17 +190,22 @@ 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,
})));
synthesizeKey("KEY_ArrowDown");
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+ if(OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ }
await new Promise(resolve => SimpleTest.executeSoon(resolve));
await triggerAutofillAndCheckProfile(MOCK_STORAGE[1].cc);
await osKeyStoreLoginShown;
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
// Fallback to history search after autofill values (for non-empty fields).
@@ -220,7 +230,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 +250,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_clear_form.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
index a1a3322c4e..4803151aae 100644
--- a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form.html
@@ -124,6 +124,11 @@ add_task(async function simple_clear() {
await triggerPopupAndHoverItem("#tel", 0);
await confirmClear("#tel");
await checkIsFormCleared();
+
+ // Ensure the correctness of the autocomplete popup after the form is cleared
+ synthesizeKey("KEY_ArrowDown");
+ await expectPopup();
+ is(4, getMenuEntries().length, `Checking length of expected menu`);
});
add_task(async function clear_adapted_record() {
@@ -154,7 +159,10 @@ add_task(async function clear_distinct_section() {
document.getElementById("form1").reset();
await triggerPopupAndHoverItem("#cc-name", 0);
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+ if(OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ }
await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE_EXPECTED_FILL[0]);
await osKeyStoreLoginShown;
@@ -175,6 +183,8 @@ add_task(async function clear_distinct_section() {
await triggerPopupAndHoverItem("#cc-name", 0);
await confirmClear("#cc-name");
await checkIsFormCleared();
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
</script>
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html
index 6ebef3bba1..f054bc5871 100644
--- a/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_clear_form_expiry_select_elements.html
@@ -127,7 +127,10 @@ add_task(async function clear_distinct_section() {
todo(false, "Cannot test OS key store login on official builds.");
return;
}
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+ if(OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ }
await triggerPopupAndHoverItem("#cc-name", 0);
await triggerAutofillAndCheckProfile(MOCK_CC_STORAGE[0]);
await osKeyStoreLoginShown;
@@ -147,6 +150,8 @@ add_task(async function clear_distinct_section() {
"cc-exp-month": "MM",
"cc-exp-year": "YY"
});
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
</script>
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/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html
index a6d0572ac6..6b317f2392 100644
--- a/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_multiple_cc_number_fields.html
@@ -143,7 +143,11 @@ add_task(async function check_filled_highlight() {
return;
}
await triggerPopupAndHoverItem("#cc-name", 0);
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ let osKeyStoreLoginShown = Promise.resolve();
+
+if (OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+}
// filled 1st credit card option
synthesizeKey("KEY_Enter");
await osKeyStoreLoginShown;
@@ -151,6 +155,8 @@ add_task(async function check_filled_highlight() {
let profile = MOCK_STORAGE_EXPECTED_FILL[0];
await setupListeners(elements, profile);
await checkMultipleCCNumberFormStyle(profile, false);
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
</script>
<p id="display"></p>
diff --git a/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html
index 090eb9290e..5517153f1a 100644
--- a/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html
+++ b/browser/extensions/formautofill/test/mochitest/creditCard/test_preview_highlight_with_site_prefill.html
@@ -87,11 +87,17 @@ add_task(async function check_filled_highlight() {
return;
}
await triggerPopupAndHoverItem("#cc-number", 0);
- let osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+
+ let osKeyStoreLoginShown = Promise.resolve();
+ if(OSKeyStore.canReauth()) {
+ osKeyStoreLoginShown = waitForOSKeyStoreLogin(true);
+ }
// filled 1st credit card option
await triggerAutofillAndCheckProfile(MOCK_STORAGE_EXPECTED_FILL[0]);
await osKeyStoreLoginShown;
await checkFormFieldsStyle(MOCK_STORAGE_EXPECTED_FILL[0], false);
+ // Enforcing this since it is unable to change back in chaos mode.
+ SpecialPowers.clearUserPref("toolkit.osKeyStore.unofficialBuildOnlyLogin");
});
</script>
<p id="display"></p>
diff --git a/browser/extensions/formautofill/test/mochitest/formautofill_common.js b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
index 6cdf9ca86b..dab2d58b4a 100644
--- a/browser/extensions/formautofill/test/mochitest/formautofill_common.js
+++ b/browser/extensions/formautofill/test/mochitest/formautofill_common.js
@@ -2,6 +2,10 @@
/* import-globals-from ../../../../../testing/mochitest/tests/SimpleTest/EventUtils.js */
/* import-globals-from ../../../../../toolkit/components/satchel/test/satchel_common.js */
/* eslint-disable no-unused-vars */
+// Despite a use of `spawnChrome` and thus ChromeUtils, we can't use isInstance
+// here as it gets used in plain mochitests which don't have the ChromeOnly
+// APIs for it.
+/* eslint-disable mozilla/use-isInstance */
"use strict";
@@ -14,6 +18,10 @@ const { FormAutofillUtils } = SpecialPowers.ChromeUtils.importESModule(
"resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
);
+const { OSKeyStore } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/OSKeyStore.sys.mjs"
+);
+
async function sleep(ms = 500, reason = "Intentionally wait for UI ready") {
SimpleTest.requestFlakyTimeout(reason);
await new Promise(resolve => setTimeout(resolve, ms));
@@ -80,8 +88,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 +279,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++) {
@@ -346,7 +361,21 @@ async function canTestOSKeyStoreLogin() {
}
async function waitForOSKeyStoreLogin(login = false) {
- await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", { login });
+ // Need to fetch this from the parent in order for it to be correct.
+ let isOSAuthEnabled = await SpecialPowers.spawnChrome([], () => {
+ // Need to re-import this because we're running in the parent.
+ // eslint-disable-next-line no-shadow
+ const { FormAutofillUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/shared/FormAutofillUtils.sys.mjs"
+ );
+
+ return FormAutofillUtils.getOSAuthEnabled(
+ FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
+ );
+ });
+ if (isOSAuthEnabled) {
+ await invokeAsyncChromeTask("FormAutofillTest:OSKeyStoreLogin", { login });
+ }
}
function patchRecordCCNumber(record) {
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/pictureinpicture/data/picture_in_picture_overrides.js b/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js
index ecc4945135..e8b690a386 100644
--- a/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js
+++ b/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js
@@ -57,7 +57,7 @@ let AVAILABLE_PIP_OVERRIDES;
aol: {
"https://*.aol.com/*": {
- videoWrapperScriptPath: "video-wrappers/yahoo.js",
+ videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
},
},
@@ -81,12 +81,41 @@ let AVAILABLE_PIP_OVERRIDES;
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
},
},
+
+ canalplus: {
+ "https://*.canalplus.com/live/*": {
+ videoWrapperScriptPath: "video-wrappers/canalplus.js",
+ disabledKeyboardControls: KEYBOARD_CONTROLS.LIVE_SEEK,
+ },
+ "https://*.canalplus.com/*": {
+ videoWrapperScriptPath: "video-wrappers/canalplus.js",
+ },
+ },
+
cbc: {
"https://*.cbc.ca/*": {
videoWrapperScriptPath: "video-wrappers/cbc.js",
},
},
+ cnbc: {
+ "https://*.cnbc.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ cpac: {
+ "https://*.cpac.ca/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ cspan: {
+ "https://*.c-span.org/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
dailymotion: {
"https://*.dailymotion.com/*": {
videoWrapperScriptPath: "video-wrappers/dailymotion.js",
@@ -105,6 +134,18 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ fandom: {
+ "https://*.fandom.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ fastcompany: {
+ "https://*.fastcompany.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
frontendMasters: {
"https://*.frontendmasters.com/*": {
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
@@ -117,6 +158,12 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ fuse: {
+ "https://*.fuse.tv/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
hbomax: {
"https://play.hbomax.com/page/*": { policy: TOGGLE_POLICIES.HIDDEN },
"https://play.hbomax.com/player/*": {
@@ -136,10 +183,34 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ imdb: {
+ "https://*.imdb.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ indpendentuk: {
+ "https://*.independent.co.uk/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ indy100: {
+ "https://*.indy100.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
instagram: {
"https://www.instagram.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER },
},
+ internetArchive: {
+ "https://*.archive.org/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
laracasts: {
"https://*.laracasts.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER },
},
@@ -149,12 +220,31 @@ let AVAILABLE_PIP_OVERRIDES;
visibilityThreshold: 0.7,
},
},
+
+ msnbc: {
+ "https://*.msnbc.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
mxplayer: {
"https://*.mxplayer.in/*": {
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
},
},
+ nbcnews: {
+ "https://*.nbcnews.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ nbcUniversal: {
+ "https://*.nbcuni.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
nebula: {
"https://*.nebula.app/*": {
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
@@ -197,6 +287,17 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ primeVideo: {
+ "https://*.primevideo.com/*": {
+ visibilityThreshold: 0.9,
+ videoWrapperScriptPath: "video-wrappers/primeVideo.js",
+ },
+ "https://*.amazon.com/*": {
+ visibilityThreshold: 0.9,
+ videoWrapperScriptPath: "video-wrappers/primeVideo.js",
+ },
+ },
+
radiocanada: {
"https://*.ici.radio-canada.ca/*": {
videoWrapperScriptPath: "video-wrappers/radiocanada.js",
@@ -207,18 +308,46 @@ let AVAILABLE_PIP_OVERRIDES;
"https://*.reddit.com/*": { policy: TOGGLE_POLICIES.ONE_QUARTER },
},
+ reuters: {
+ "https://*.reuters.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
sonyliv: {
"https://*.sonyliv.com/*": {
videoWrapperScriptPath: "video-wrappers/sonyliv.js",
},
},
+ syfy: {
+ "https://*.syfy.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
ted: {
"https://*.ted.com/*": {
showHiddenTextTracks: true,
},
},
+ time: {
+ "https://*.time.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
+ timvision: {
+ "https://*.timvision.it/TV/*": {
+ videoWrapperScriptPath: "video-wrappers/canalplus.js",
+ disabledKeyboardControls: KEYBOARD_CONTROLS.LIVE_SEEK,
+ },
+ "https://*.timvision.it/*": {
+ videoWrapperScriptPath: "video-wrappers/canalplus.js",
+ },
+ },
+
tubi: {
"https://*.tubitv.com/live*": {
videoWrapperScriptPath: "video-wrappers/tubilive.js",
@@ -256,6 +385,12 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
+ univision: {
+ "https://*.univision.com/*": {
+ videoWrapperScriptPath: "video-wrappers/jwplayerWrapper.js",
+ },
+ },
+
viki: {
"https://*.viki.com/*": {
videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
@@ -274,9 +409,9 @@ let AVAILABLE_PIP_OVERRIDES;
},
},
- yahoofinance: {
- "https://*.finance.yahoo.com/*": {
- videoWrapperScriptPath: "video-wrappers/yahoo.js",
+ yahoo: {
+ "https://*.s.yimg.com/*": {
+ videoWrapperScriptPath: "video-wrappers/videojsWrapper.js",
},
},
@@ -301,16 +436,5 @@ let AVAILABLE_PIP_OVERRIDES;
videoWrapperScriptPath: "video-wrappers/washingtonpost.js",
},
},
-
- primeVideo: {
- "https://*.primevideo.com/*": {
- visibilityThreshold: 0.9,
- videoWrapperScriptPath: "video-wrappers/primeVideo.js",
- },
- "https://*.amazon.com/*": {
- visibilityThreshold: 0.9,
- videoWrapperScriptPath: "video-wrappers/primeVideo.js",
- },
- },
};
}
diff --git a/browser/extensions/pictureinpicture/moz.build b/browser/extensions/pictureinpicture/moz.build
index 7cc77f9594..fbdefbeb1c 100644
--- a/browser/extensions/pictureinpicture/moz.build
+++ b/browser/extensions/pictureinpicture/moz.build
@@ -31,6 +31,7 @@ FINAL_TARGET_FILES.features["pictureinpicture@mozilla.org"]["video-wrappers"] +=
"video-wrappers/airmozilla.js",
"video-wrappers/arte.js",
"video-wrappers/bbc.js",
+ "video-wrappers/canalplus.js",
"video-wrappers/cbc.js",
"video-wrappers/dailymotion.js",
"video-wrappers/disneyplus.js",
@@ -38,6 +39,7 @@ FINAL_TARGET_FILES.features["pictureinpicture@mozilla.org"]["video-wrappers"] +=
"video-wrappers/hbomax.js",
"video-wrappers/hotstar.js",
"video-wrappers/hulu.js",
+ "video-wrappers/jwplayerWrapper.js",
"video-wrappers/mock-wrapper.js",
"video-wrappers/netflix.js",
"video-wrappers/nytimes.js",
@@ -52,7 +54,6 @@ FINAL_TARGET_FILES.features["pictureinpicture@mozilla.org"]["video-wrappers"] +=
"video-wrappers/videojsWrapper.js",
"video-wrappers/voot.js",
"video-wrappers/washingtonpost.js",
- "video-wrappers/yahoo.js",
"video-wrappers/youtube.js",
]
diff --git a/browser/extensions/pictureinpicture/video-wrappers/canalplus.js b/browser/extensions/pictureinpicture/video-wrappers/canalplus.js
new file mode 100644
index 0000000000..3d725ef54a
--- /dev/null
+++ b/browser/extensions/pictureinpicture/video-wrappers/canalplus.js
@@ -0,0 +1,56 @@
+/* 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";
+
+class PictureInPictureVideoWrapper {
+ isLive() {
+ let documentURI = document.documentURI;
+ return documentURI.includes("/live/") || documentURI.includes("/TV/");
+ }
+
+ getDuration(video) {
+ if (this.isLive(video)) {
+ return Infinity;
+ }
+ return video.duration;
+ }
+
+ setCaptionContainerObserver(video, updateCaptionsFunction) {
+ let container =
+ document.querySelector(`[data-testid="playerRoot"]`) ||
+ document.querySelector(`[player-root="true"]`);
+
+ if (container) {
+ updateCaptionsFunction("");
+ const callback = function (mutationsList) {
+ // eslint-disable-next-line no-unused-vars
+ for (const mutation of mutationsList) {
+ let text = container.querySelector(
+ ".rxp-texttrack-region"
+ )?.innerText;
+ if (!text) {
+ updateCaptionsFunction("");
+ return;
+ }
+
+ updateCaptionsFunction(text);
+ }
+ };
+
+ // immediately invoke the callback function to add subtitles to the PiP window
+ callback([1], null);
+
+ let captionsObserver = new MutationObserver(callback);
+
+ captionsObserver.observe(container, {
+ attributes: false,
+ childList: true,
+ subtree: true,
+ });
+ }
+ }
+}
+
+this.PictureInPictureVideoWrapper = PictureInPictureVideoWrapper;
diff --git a/browser/extensions/pictureinpicture/video-wrappers/yahoo.js b/browser/extensions/pictureinpicture/video-wrappers/jwplayerWrapper.js
index 1dd932bc37..37591c16f8 100644
--- a/browser/extensions/pictureinpicture/video-wrappers/yahoo.js
+++ b/browser/extensions/pictureinpicture/video-wrappers/jwplayerWrapper.js
@@ -4,15 +4,16 @@
"use strict";
+// This wrapper supports multiple sites that use JWPlayer
class PictureInPictureVideoWrapper {
setCaptionContainerObserver(video, updateCaptionsFunction) {
- let container = document.querySelector(".vp-main");
+ let container = document.querySelector(".jw-captions");
if (container) {
updateCaptionsFunction("");
- const callback = function () {
- let text = container.querySelector(".vp-cc-element.vp-show")?.innerText;
+ const callback = function () {
+ let text = container.innerText;
if (!text) {
updateCaptionsFunction("");
return;
@@ -22,7 +23,7 @@ class PictureInPictureVideoWrapper {
};
// immediately invoke the callback function to add subtitles to the PiP window
- callback([1], null);
+ callback();
let captionsObserver = new MutationObserver(callback);
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
new file mode 100644
index 0000000000..fe82d3e03a
--- /dev/null
+++ b/browser/installer/windows/docs/newProjectDllVS.png
Binary files differ
diff --git a/browser/installer/windows/docs/projectPropertyPageVS.png b/browser/installer/windows/docs/projectPropertyPageVS.png
new file mode 100644
index 0000000000..3d1ecb6083
--- /dev/null
+++ b/browser/installer/windows/docs/projectPropertyPageVS.png
Binary files differ
diff --git a/browser/installer/windows/docs/projectSettingsDllVS.png b/browser/installer/windows/docs/projectSettingsDllVS.png
new file mode 100644
index 0000000000..59329df681
--- /dev/null
+++ b/browser/installer/windows/docs/projectSettingsDllVS.png
Binary files differ
diff --git a/browser/installer/windows/msix/AppxManifest.xml.in b/browser/installer/windows/msix/AppxManifest.xml.in
index f1c3b6b721..2e11b5d34b 100644
--- a/browser/installer/windows/msix/AppxManifest.xml.in
+++ b/browser/installer/windows/msix/AppxManifest.xml.in
@@ -91,18 +91,6 @@
<uap:Logo>Assets\Document44x44.png</uap:Logo>
</uap3:Protocol>
</uap3:Extension>
- <uap3:Extension Category="windows.protocol">
- <uap3:Protocol Name="firefox-bridge" Parameters="-osint -url &quot;%1&quot;">
- <uap:DisplayName>Firefox Bridge Protocol</uap:DisplayName>
- <uap:Logo>Assets\Document44x44.png</uap:Logo>
- </uap3:Protocol>
- </uap3:Extension>
- <uap3:Extension Category="windows.protocol">
- <uap3:Protocol Name="firefox-private-bridge" Parameters="-osint -private-window &quot;%1&quot;">
- <uap:DisplayName>Firefox Private Bridge Protocol</uap:DisplayName>
- <uap:Logo>Assets\Document44x44.png</uap:Logo>
- </uap3:Protocol>
- </uap3:Extension>
<!-- COM registrations for the notification server. -->
<com:Extension Category="windows.comServer">
<com:ComServer>
diff --git a/browser/installer/windows/nsis/defines.nsi.in b/browser/installer/windows/nsis/defines.nsi.in
index cbcb2e9be0..ae17ff4d17 100644
--- a/browser/installer/windows/nsis/defines.nsi.in
+++ b/browser/installer/windows/nsis/defines.nsi.in
@@ -54,11 +54,11 @@
!define IDI_PBICON_PB_EXE_ZERO_BASED "0"
!define CERTIFICATE_NAME "Mozilla Corporation"
-!define CERTIFICATE_ISSUER "DigiCert SHA2 Assured ID Code Signing CA"
+!define CERTIFICATE_ISSUER "DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1"
; Changing the name or issuer requires us to have both the old and the new
; in the registry at the same time, temporarily.
!define CERTIFICATE_NAME_PREVIOUS "Mozilla Corporation"
-!define CERTIFICATE_ISSUER_PREVIOUS "DigiCert Assured ID Code Signing CA-1"
+!define CERTIFICATE_ISSUER_PREVIOUS "DigiCert SHA2 Assured ID Code Signing CA"
# LSP_CATEGORIES is the permitted LSP categories for the application. Each LSP
# category value is ANDed together to set multiple permitted categories.
diff --git a/browser/installer/windows/nsis/installer.nsi b/browser/installer/windows/nsis/installer.nsi
index c282067697..ed64931a66 100755
--- a/browser/installer/windows/nsis/installer.nsi
+++ b/browser/installer/windows/nsis/installer.nsi
@@ -474,25 +474,6 @@ Section "-Application" APP_IDX
${AddDisabledDDEHandlerValues} "FirefoxURL-$AppUserModelID" "$2" "$8,${IDI_DOCUMENT_ZERO_BASED}" \
"${AppRegName} URL" "true"
- ; Create protocol registry keys for FirefoxBridge extensions - only if not already set
- SetShellVarContext current ; Set SHCTX to HKCU
- !define FIREFOX_PROTOCOL "firefox-bridge"
- ClearErrors
- ReadRegStr $0 SHCTX "Software\Classes\${FIREFOX_PROTOCOL}" ""
- ${If} $0 == ""
- ${AddDisabledDDEHandlerValues} "${FIREFOX_PROTOCOL}" "$2" "$8,${IDI_APPICON_ZERO_BASED}" \
- "Firefox Bridge Protocol" "true"
- ${EndIf}
-
- !define FIREFOX_PRIVATE_PROTOCOL "firefox-private-bridge"
- ClearErrors
- ReadRegStr $0 SHCTX "Software\Classes\${FIREFOX_PRIVATE_PROTOCOL}" ""
- ${If} $0 == ""
- ${AddDisabledDDEHandlerValues} "${FIREFOX_PRIVATE_PROTOCOL}" "$\"$8$\" -osint -private-window $\"%1$\"" \
- "$8,${IDI_PBICON_PB_EXE_ZERO_BASED}" "Firefox Private Bridge Protocol" "true"
- ${EndIf}
- SetShellVarContext all ; Set SHCTX to HKLM
-
; The keys below can be set in HKCU if needed.
${If} $TmpVal == "HKLM"
; Set the Start Menu Internet and Registered App HKLM registry keys.
diff --git a/browser/installer/windows/nsis/maintenanceservice_installer.nsi b/browser/installer/windows/nsis/maintenanceservice_installer.nsi
index c285e45bbd..5d50ee9e6c 100644
--- a/browser/installer/windows/nsis/maintenanceservice_installer.nsi
+++ b/browser/installer/windows/nsis/maintenanceservice_installer.nsi
@@ -217,7 +217,7 @@ Section "MaintenanceService"
; These keys are used to bypass the installation dir is a valid installation
; check from the service so that tests can be run.
; WriteRegStr HKLM "${FallbackKey}\0" "name" "Mozilla Corporation"
- ; WriteRegStr HKLM "${FallbackKey}\0" "issuer" "DigiCert SHA2 Assured ID Code Signing CA"
+ ; WriteRegStr HKLM "${FallbackKey}\0" "issuer" "DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1"
${If} ${RunningX64}
${OrIf} ${IsNativeARM64}
SetRegView lastused
diff --git a/browser/installer/windows/nsis/shared.nsh b/browser/installer/windows/nsis/shared.nsh
index ccad601abe..f9f50a5afa 100755
--- a/browser/installer/windows/nsis/shared.nsh
+++ b/browser/installer/windows/nsis/shared.nsh
@@ -112,6 +112,14 @@
Pop $TmpVal ; get "Marker"
${EndIf}
+ ClearErrors
+ WriteRegStr HKLM "Software\Mozilla" "${BrandShortName}InstallerTest" "Write Test"
+ ${If} ${Errors}
+ StrCpy $TmpVal "HKCU"
+ ${Else}
+ StrCpy $TmpVal "HKLM"
+ ${EndIf}
+
!ifdef MOZ_MAINTENANCE_SERVICE
Call IsUserAdmin
Pop $R0
diff --git a/browser/locales-preview/backupSettings.ftl b/browser/locales-preview/backupSettings.ftl
new file mode 100644
index 0000000000..351a60998b
--- /dev/null
+++ b/browser/locales-preview/backupSettings.ftl
@@ -0,0 +1,5 @@
+# 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/.
+
+settings-data-backup-header = Backup
diff --git a/browser/locales-preview/select-translations.ftl b/browser/locales-preview/select-translations.ftl
deleted file mode 100644
index 731abaff1a..0000000000
--- a/browser/locales-preview/select-translations.ftl
+++ /dev/null
@@ -1,51 +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 https://mozilla.org/MPL/2.0/.
-
-
-# Text displayed in the right-click context menu for translating
-# selected text to a yet-to-be-determined language.
-main-context-menu-translate-selection =
- .label = Translate Selection…
-
-# Text displayed in the right-click context menu for translating
-# selected text to a target language.
-#
-# Variables:
-# $language (string) - The localized display name of the target language
-main-context-menu-translate-selection-to-language =
- .label = Translate Selection to { $language }
-
-# Text displayed in the right-click context menu for translating
-# the text of a hyperlink to a yet-to-be-determined language.
-main-context-menu-translate-link-text =
- .label = Translate Link Text…
-
-# Text displayed in the right-click context menu for translating
-# the text of a hyperlink to a target language.
-#
-# Variables:
-# $language (string) - The localized display name of the target language
-main-context-menu-translate-link-text-to-language =
- .label = Translate Link Text to { $language }
-
-# Text displayed in the select translations panel header.
-select-translations-panel-header = Translation
-
-# Text displayed above the from-language dropdown menu.
-select-translations-panel-from-label = From
-
-# Text displayed above the to-language dropdown menu.
-select-translations-panel-to-label = To
-
-# Text displayed on the copy button.
-select-translations-panel-copy-button = Copy
-
-# Text displayed on the done button.
-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.
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/aboutLogins.ftl b/browser/locales/en-US/browser/aboutLogins.ftl
index f18478c7a7..514dc05089 100644
--- a/browser/locales/en-US/browser/aboutLogins.ftl
+++ b/browser/locales/en-US/browser/aboutLogins.ftl
@@ -140,6 +140,14 @@ about-logins-os-auth-dialog-caption = { -brand-full-name }
## and includes subtitle of "Enter password for the user "xxx" to allow this." These
## notes are only valid for English. Please test in your respected locale.
+# The macOS strings are preceded by the operating system with "Firefox is trying to ".
+# This message can be seen when attempting to disable osauth in about:preferences.
+about-logins-os-auth-dialog-message=
+ { PLATFORM() ->
+ [macos] change the settings for passwords
+ *[other] { -brand-short-name } is trying to change the settings for passwords. Use your device sign in to allow this.
+ }
+
# This message can be seen when attempting to edit a login in about:logins on Windows.
about-logins-edit-login-os-auth-dialog-message2-win = To edit your password, enter your Windows login credentials. This helps protect the security of your accounts.
# This message can be seen when attempting to edit a login in about:logins
diff --git a/browser/locales/en-US/browser/accounts.ftl b/browser/locales/en-US/browser/accounts.ftl
index 21f95d0b14..47433ddea7 100644
--- a/browser/locales/en-US/browser/accounts.ftl
+++ b/browser/locales/en-US/browser/accounts.ftl
@@ -49,7 +49,7 @@ account-send-tab-to-device-verify = Verify Your Account…
# The title shown in a notification when either this device or another device
# has connected to, or disconnected from, a Firefox account.
-account-connection-title = { -fxaccount-brand-name(capitalization: "title") }
+account-connection-title-2 = Account
# Variables:
# $deviceName (String): the name of the new device
diff --git a/browser/locales/en-US/browser/allTabsMenu.ftl b/browser/locales/en-US/browser/allTabsMenu.ftl
index 019c22361b..92212fb55e 100644
--- a/browser/locales/en-US/browser/allTabsMenu.ftl
+++ b/browser/locales/en-US/browser/allTabsMenu.ftl
@@ -11,3 +11,6 @@ all-tabs-menu-new-user-context =
all-tabs-menu-hidden-tabs =
.label = Hidden tabs
+
+all-tabs-menu-close-duplicate-tabs =
+ .label = Close duplicate tabs
diff --git a/browser/locales/en-US/browser/appmenu.ftl b/browser/locales/en-US/browser/appmenu.ftl
index 5ad2d75f62..d26566ebb7 100644
--- a/browser/locales/en-US/browser/appmenu.ftl
+++ b/browser/locales/en-US/browser/appmenu.ftl
@@ -81,19 +81,13 @@ 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.
-# Variables
-# $count (Number) - The number of inactive tabs which are not being shown (at least 1)
-appmenu-remote-tabs-showinactive =
- .label =
- { $count ->
- [one] Show one inactive tab
- *[other] Show { $count } inactive tabs
- }
- .tooltiptext = Show the inactive tabs on this device
+# This is shown as the label for an element to show inactive tabs from this device.
+appmenu-remote-tabs-show-inactive-tabs =
+ .label = Inactive tabs
+ .tooltiptext = See inactive tabs on this device
# This is shown beneath the name of a device when that device has no open tabs
appmenu-remote-tabs-notabs = No open tabs
diff --git a/browser/locales/en-US/browser/browser.ftl b/browser/locales/en-US/browser/browser.ftl
index 02ba7bb1fa..9fbdcba15a 100644
--- a/browser/locales/en-US/browser/browser.ftl
+++ b/browser/locales/en-US/browser/browser.ftl
@@ -638,6 +638,12 @@ urlbar-result-action-copy-to-clipboard = Copy
# $result (String): the string representation for a formula result
urlbar-result-action-calculator-result = = { $result }
+## Strings used for buttons in the urlbar
+
+# Label prompting user to search with a particular search engine.
+# $engine (String): the name of a search engine that searches a specific site
+urlbar-result-search-with = Search with { $engine }
+
## Action text shown in urlbar results, usually appended after the search
## string or the url, like "result value - action text".
## In these actions "Search" is a verb, followed by where the search is performed.
@@ -970,6 +976,16 @@ data-reporting-notification-button =
# Label for the indicator shown in the private browsing window titlebar.
private-browsing-indicator-label = Private browsing
+# Tooltip for the indicator shown in the window titlebar when content analysis is active.
+# Variables:
+# $agentName (String): The name of the DLP agent that is connected
+content-analysis-indicator-tooltip =
+ .tooltiptext = Data loss prevention (DLP) by { $agentName }. Click for more info.
+content-analysis-panel-title = Data protection
+# Variables:
+# $agentName (String): The name of the DLP agent that is connected
+content-analysis-panel-text = Your organization uses { $agentName } to protect against data loss. <a data-l10n-name="info">Learn more</a>
+
## Unified extensions (toolbar) button
unified-extensions-button =
diff --git a/browser/locales/en-US/browser/confirmationHints.ftl b/browser/locales/en-US/browser/confirmationHints.ftl
index 2da1e317cd..3e03977a3b 100644
--- a/browser/locales/en-US/browser/confirmationHints.ftl
+++ b/browser/locales/en-US/browser/confirmationHints.ftl
@@ -18,3 +18,10 @@ confirmation-hint-send-to-device = Sent!
confirmation-hint-firefox-relay-mask-created = New mask created!
confirmation-hint-firefox-relay-mask-reused = Existing mask reused!
confirmation-hint-screenshot-copied = Screenshot copied!
+# Variables:
+# $tabCount (Number): The number of duplicate tabs closed, at least 1.
+confirmation-hint-duplicate-tabs-closed =
+ { $tabCount ->
+ [one] Closed { $tabCount } tab
+ *[other] Closed { $tabCount } tabs
+ }
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/asrouter.ftl b/browser/locales/en-US/browser/newtab/asrouter.ftl
index c4c94c9c7d..2bfad6833d 100644
--- a/browser/locales/en-US/browser/newtab/asrouter.ftl
+++ b/browser/locales/en-US/browser/newtab/asrouter.ftl
@@ -69,7 +69,7 @@ cfr-doorhanger-extension-total-users =
## Firefox Accounts Message
cfr-doorhanger-bookmark-fxa-header = Sync your bookmarks everywhere.
-cfr-doorhanger-bookmark-fxa-body = Great find! Now don’t be left without this bookmark on your mobile devices. Get Started with a { -fxaccount-brand-name }.
+cfr-doorhanger-bookmark-fxa-body-2 = Great find! Now don’t be left without this bookmark on your mobile devices. Get started with an account.
cfr-doorhanger-bookmark-fxa-link-text = Sync bookmarks now…
cfr-doorhanger-bookmark-fxa-close-btn-tooltip =
.aria-label = Close button
diff --git a/browser/locales/en-US/browser/newtab/newtab.ftl b/browser/locales/en-US/browser/newtab/newtab.ftl
index 256a8da576..7e99cf25c9 100644
--- a/browser/locales/en-US/browser/newtab/newtab.ftl
+++ b/browser/locales/en-US/browser/newtab/newtab.ftl
@@ -270,5 +270,59 @@ newtab-custom-pocket-show-recent-saves = Show recent saves
newtab-custom-recent-toggle =
.label = Recent activity
.description = A selection of recent sites and content
+newtab-custom-weather-toggle =
+ .label = Weather
+ .description = Today’s forecast at a glance
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>
+
+## New Tab Weather
+
+# Variables:
+# $provider (string) - Service provider for weather data
+newtab-weather-see-forecast =
+ .title = See forecast in { $provider }
+# Variables:
+# $provider (string) - Service provider for weather data
+newtab-weather-sponsored = { $provider } ∙ Sponsored
+newtab-weather-menu-change-location = Change location
+newtab-weather-change-location-search-input = Search location
+newtab-weather-menu-weather-display = Weather display
+# Display options are:
+# - Simple: Displays a current weather condition icon and the current temperature
+# - Detailed: Include simple information plus a short text summary: e.g. "Mostly cloudy"
+newtab-weather-menu-weather-display-option-simple = Simple
+newtab-weather-menu-change-weather-display-simple = Switch to simple view
+newtab-weather-menu-weather-display-option-detailed = Detailed
+newtab-weather-menu-change-weather-display-detailed = Switch to detailed view
+newtab-weather-menu-temperature-units = Temperature units
+newtab-weather-menu-temperature-option-fahrenheit = Fahrenheit
+newtab-weather-menu-temperature-option-celsius = Celsius
+newtab-weather-menu-change-temperature-units-fahrenheit = Switch to Fahrenheit
+newtab-weather-menu-change-temperature-units-celsius = Switch to Celsius
+newtab-weather-menu-hide-weather = Hide weather on New Tab
+newtab-weather-menu-learn-more = Learn more
+# This message is shown if user is working offline
+newtab-weather-error-not-available = Weather data is not available right now.
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..f846acc3d9 100644
--- a/browser/locales/en-US/browser/policies/policies-descriptions.ftl
+++ b/browser/locales/en-US/browser/policies/policies-descriptions.ftl
@@ -66,9 +66,12 @@ policy-DisableDefaultBrowserAgent = Prevent the default browser agent from takin
policy-DisableDeveloperTools = Block access to the developer tools.
+policy-DisableEncryptedClientHello = Disable use of the TLS feature Encrypted Client Hello (ECH).
+
policy-DisableFeedbackCommands = Disable commands to send feedback from the Help menu (Submit Feedback and Report Deceptive Site).
-policy-DisableFirefoxAccounts = Disable { -fxaccount-brand-name } based services, including Sync.
+# This string is in the process of being deprecated in favor of policy-DisableAccounts.
+policy-DisableFirefoxAccounts1 = Disable account-based services, including sync.
# Firefox Screenshots is the name of the feature, and should not be translated.
policy-DisableFirefoxScreenshots = Disable the Firefox Screenshots feature.
@@ -143,6 +146,10 @@ policy-HardwareAcceleration = If false, turn off hardware acceleration.
# “lock” means that the user won’t be able to change this setting
policy-Homepage = Set and optionally lock the homepage.
+policy-HttpAllowlist = Origins that will not be upgraded to HTTPS.
+
+policy-HttpsOnlyMode = Allow HTTPS-Only Mode to be enabled.
+
policy-InstallAddonsPermission = Allow certain websites to install add-ons.
policy-LegacyProfiles = Disable the feature enforcing a separate profile for each installation.
@@ -183,6 +190,10 @@ policy-PasswordManagerEnabled = Enable saving passwords to the password manager.
policy-PasswordManagerExceptions = Prevent { -brand-short-name } from saving passwords for specific sites.
+# Post-quantum refers to cryptography that is safe from attacks by quantum
+# computers. See https://en.wikipedia.org/wiki/Post-quantum_cryptography
+policy-PostQuantumKeyAgreementEnabled = Enable post-quantum key agreement for TLS.
+
# PDF.js and PDF should not be translated
policy-PDFjs = Disable or configure PDF.js, the built-in PDF viewer in { -brand-short-name }.
@@ -221,6 +232,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..3a4e0766a7 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
@@ -732,6 +734,10 @@ home-prefs-highlights-option-saved-to-pocket =
home-prefs-recent-activity-header =
.label = Recent activity
home-prefs-recent-activity-description = A selection of recent sites and content
+home-prefs-weather-header =
+ .label = Weather
+home-prefs-weather-description = Today’s forecast at a glance
+home-prefs-weather-learn-more-link = Learn more
# Variables:
# $num (number) - Number of rows displayed
@@ -853,8 +859,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…
@@ -1027,6 +1040,9 @@ forms-saved-passwords =
forms-primary-pw-use =
.label = Use a Primary Password
.accesskey = U
+# This operation requires the user to authenticate with the operating system (device sign-in)
+forms-os-reauth =
+ .label = Require device sign in to fill and manage passwords
forms-primary-pw-learn-more-link = Learn more
# This string uses the former name of the Primary Password feature
# ("Master Password" in English) so that the preferences can be found
@@ -1063,6 +1079,13 @@ primary-password-os-auth-dialog-message-win = To create a Primary Password, ente
primary-password-os-auth-dialog-message-macosx = create a Primary Password
master-password-os-auth-dialog-caption = { -brand-full-name }
+# The macOS string is preceded by the operating system with "Firefox is trying to ".
+autofill-creditcard-os-dialog-message = { PLATFORM () ->
+ [macos] change the settings for payment methods
+ *[other] { -brand-short-name } is trying to change the settings for payment methods. Use your device sign in to allow this.
+}
+autofill-creditcard-os-auth-dialog-caption = { -brand-full-name }
+
## Privacy section - Autofill
pane-privacy-autofill-header = Autofill
@@ -1076,12 +1099,9 @@ autofill-payment-methods-checkbox-submessage = Includes credit and debit cards
.accesskey = I
autofill-saved-payment-methods-button = Saved payment methods
.accesskey = v
-autofill-reauth-checkbox = { PLATFORM() ->
- [macos] Require macOS authentication to fill and edit payment methods.
- [windows] Require Windows authentication to fill and edit payment methods.
- [linux] Require Linux authentication to fill and edit payment methods.
- *[other] Require authentication to fill and edit payment methods.
- }
+
+# This operation requires the user to authenticate with the operating system (device sign-in)
+autofill-reauth-payment-methods-checkbox = Require device sign in to fill and manage payment methods
.accesskey = o
## Privacy Section - History
@@ -1586,8 +1606,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..b7ecece63b 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,42 @@ 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-2 = Copy
+ .title = Copy ({ $shortcut })
+ .aria-label = Copy
+
+# Variables
+# $shortcut (String) - A keyboard shortcut for saving/downloading the screenshot.
+screenshots-component-download-button-2 = Download
+ .title = Download ({ $shortcut })
+ .aria-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
+# "×" 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-3 = { $width } × { $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/tabContextMenu.ftl b/browser/locales/en-US/browser/tabContextMenu.ftl
index df58df794c..5ace34fd8a 100644
--- a/browser/locales/en-US/browser/tabContextMenu.ftl
+++ b/browser/locales/en-US/browser/tabContextMenu.ftl
@@ -72,6 +72,9 @@ move-to-new-window =
tab-context-close-multiple-tabs =
.label = Close Multiple Tabs
.accesskey = M
+tab-context-close-duplicate-tabs =
+ .label = Close Duplicate Tabs
+ .accesskey = u
tab-context-share-url =
.label = Share
.accesskey = h
diff --git a/browser/locales/en-US/browser/tabbrowser.ftl b/browser/locales/en-US/browser/tabbrowser.ftl
index 21e4897ac5..58f1a7c453 100644
--- a/browser/locales/en-US/browser/tabbrowser.ftl
+++ b/browser/locales/en-US/browser/tabbrowser.ftl
@@ -120,6 +120,11 @@ tabbrowser-confirm-caretbrowsing-title = Caret Browsing
tabbrowser-confirm-caretbrowsing-message = Pressing F7 turns Caret Browsing on or off. This feature places a moveable cursor in web pages, allowing you to select text with the keyboard. Do you want to turn Caret Browsing on?
tabbrowser-confirm-caretbrowsing-checkbox = Do not show me this dialog box again.
+## Confirmation dialog for closing all duplicate tabs
+
+tabbrowser-confirm-close-duplicate-tabs-title = Heads up
+tabbrowser-confirm-close-duplicate-tabs-text = We’ll keep open the last active tab
+
##
# Variables:
diff --git a/browser/locales/en-US/browser/translations.ftl b/browser/locales/en-US/browser/translations.ftl
index 8483a4591a..efd9353d99 100644
--- a/browser/locales/en-US/browser/translations.ftl
+++ b/browser/locales/en-US/browser/translations.ftl
@@ -123,19 +123,19 @@ translations-manage-header = Translations
translations-manage-settings-button =
.label = Settings…
.accesskey = t
-translations-manage-intro = Set your language and site translation preferences and manage languages installed for offline translation.
-translations-manage-install-description = Install languages for offline translation
-translations-manage-language-install-button =
- .label = Install
-translations-manage-language-install-all-button =
- .label = Install all
- .accesskey = I
+translations-manage-intro-2 = Set your language and site translation preferences and manage languages downloaded for offline translation.
+translations-manage-download-description = Download languages for offline translation
+translations-manage-language-download-button =
+ .label = Download
+translations-manage-language-download-all-button =
+ .label = Download all
+ .accesskey = D
translations-manage-language-remove-button =
.label = Remove
translations-manage-language-remove-all-button =
.label = Remove all
.accesskey = e
-translations-manage-error-install = There was a problem installing the language files. Please try again.
+translations-manage-error-download = There was a problem downloading the language files. Please try again.
translations-manage-error-remove = There was an error removing the language files. Please try again.
translations-manage-error-list = Failed to get the list of available languages for translation. Refresh the page to try again.
@@ -166,3 +166,96 @@ translations-settings-remove-all-sites-button =
translations-settings-close-dialog =
.buttonlabelaccept = Close
.buttonaccesskeyaccept = C
+
+# Text displayed in the right-click context menu for translating
+# selected text to a yet-to-be-determined language.
+main-context-menu-translate-selection =
+ .label = Translate Selection…
+ .accesskey = n
+
+# Text displayed in the right-click context menu for translating
+# selected text to a target language.
+#
+# Variables:
+# $language (string) - The localized display name of the target language
+main-context-menu-translate-selection-to-language =
+ .label = Translate Selection to { $language }
+ .accesskey = n
+
+# Text displayed in the right-click context menu for translating
+# the text of a hyperlink to a yet-to-be-determined language.
+main-context-menu-translate-link-text =
+ .label = Translate Link Text…
+ .accesskey = n
+
+# Text displayed in the right-click context menu for translating
+# the text of a hyperlink to a target language.
+#
+# Variables:
+# $language (string) - The localized display name of the target language
+main-context-menu-translate-link-text-to-language =
+ .label = Translate Link Text to { $language }
+ .accesskey = n
+
+# Text displayed in the select translations panel header.
+select-translations-panel-header = Translation
+
+# Text displayed above the from-language dropdown menu.
+select-translations-panel-from-label = From
+
+# Text displayed above the to-language dropdown menu.
+select-translations-panel-to-label = To
+
+# Text displayed above the try-another-source-language dropdown menu.
+select-translations-panel-try-another-language-label = Try another source language
+
+select-translations-panel-cancel-button =
+ .label = Cancel
+
+# Text displayed on the copy button before it is clicked.
+select-translations-panel-copy-button =
+ .label = Copy
+
+# Text displayed on the copy button after it is clicked.
+select-translations-panel-copy-button-copied =
+ .label = Copied
+
+select-translations-panel-done-button =
+ .label = Done
+
+select-translations-panel-translate-full-page-button =
+ .label = Translate full page
+
+select-translations-panel-translate-button =
+ .label = Translate
+
+select-translations-panel-try-again-button =
+ .label = Try again
+
+# 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…
+
+select-translations-panel-init-failure-message =
+ .message = Couldn’t load languages. Check your internet connection and try again.
+
+# Text displayed when the translation fails to complete.
+select-translations-panel-translation-failure-message =
+ .message = There was a problem translating. Please try again.
+
+# If your language requires declining the language name, a possible solution
+# is to adapt the structure of the phrase, or use a support noun, e.g.
+# `Sorry, we don't support the language yet: { $language }
+#
+# Variables:
+# $language (string) - The language of the document.
+select-translations-panel-unsupported-language-message-known =
+ .message = Sorry, we don’t support { $language } yet.
+select-translations-panel-unsupported-language-message-unknown =
+ .message = Sorry, we don’t support this language yet.
+
+# Text displayed on the menuitem that opens the Translation Settings page.
+select-translations-panel-open-translations-settings-menuitem =
+ .label = Translation settings
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..f6441c6eb1 100644
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -13,11 +13,12 @@
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)
browser (%browser/**/*.ftl)
preview/profiles.ftl (../components/profiles/content/profiles.ftl)
+ preview/backupSettings.ftl (../locales-preview/backupSettings.ftl)
@AB_CD@.jar:
% locale browser @AB_CD@ %locale/browser/
diff --git a/browser/locales/l10n-changesets.json b/browser/locales/l10n-changesets.json
index 63c677409f..a42b1a7112 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": "30c7ab8f98a0277ddad0ce83463f8e7fa72c1362"
},
"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": "d00cb8760024caf0f84934077e566b61d737c137"
},
"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": "832bbf7a966a76e3ea9925d9a0ae14ba144cd53f"
},
"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": "ba7d58b38a83bbcd83a80d0d7f2097fc217b85ad"
},
"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": "39847764e40b764c0a3cfc5fe9e79a212b318f22"
},
"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": "02038541bd0ea8e2068ca347f1de279924f8c783"
},
"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": "07130d144c99bdd0243d0e561085296cc374f1aa"
},
"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": "b2638420bae6d12244ae9dfdccb6fbbe8dc1ec4c"
},
"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": "7474888bcdc92f8e9b5ad7c7e3050051b1094032"
},
"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": "3378e63eeb73ca1e8813fcca5c63095ca62d5a72"
},
"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": "22b883ebca9ae1acfd89ff30d8e3f2e13a8faae4"
},
"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": "17852617a30266665f460dff84275c824b0c03ca"
},
"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": "8506cf9e09a0b242ee723adce6e8aa3ff1ee306d"
},
"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": "a51fae82ae2bbf6e7ce54abab41d07d9ee16366a"
},
"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": "3280e4158cdc53eba3df93c4ace72afe7468a00e"
},
"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": "4a9c16d55e4777a71e46a6c82550ac175d34863b"
},
"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": "87467354c0a962ffc289694ab85a2d73ec05dc8e"
},
"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": "198642b1dcd67f12759fe4e36b13e26fa6503bc8"
},
"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": "a23e87ca4d1ce11d9d13c274d19288caf1e55989"
},
"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": "2e5173f9ffae2ba50fbfbdee0b0cd56ec97fdc94"
},
"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": "0d3c05ba7f3d3266f31ae1b90a4d71588069cdbf"
},
"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": "cf5e70df290ae3fb344772dccd3e166a3fd6162c"
},
"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": "7bd3f304113a95a6c25443975adca58ea6ee6fca"
},
"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": "6c220521a32cd518b8e3c0530c01060196d56533"
},
"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": "722f6873e64de644c6f499c8adab43d78d7a6c4c"
},
"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": "e359ed19862627981b77ae891bc8dd7b788ba6c4"
},
"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": "495f3c8e5c60abdd3375794e9b6dc358b654bb1e"
},
"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": "4687e38a10efb0c70cccffb1c7b14d20f47d6a9b"
},
"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": "221fe0a1c30dfcd02b3ad2b1389f9f29fa6ec7c1"
},
"ja-JP-mac": {
"pin": false,
@@ -921,7 +972,7 @@
"macosx64",
"macosx64-devedition"
],
- "revision": "6a7a8218314e397ea975f8229d25946d71b59f93"
+ "revision": "7f11e4534f4b9d96d01877430f493f872ed81823"
},
"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": "a8b410da592b85dd0c75e0cfe65d051f7c14240a"
},
"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": "f53ed786aab283e226c71adbf4076595d4935645"
},
"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": "36a1b873ebeb28fcb73455aa80ea2a7e5d526c5a"
},
"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": "8096f3082dd1703d6227842b4b9b8c185831edae"
},
"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": "50bccc5047eec4c23381d328dbbb5a178ed552c0"
},
"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": "d3c7e5f4e5b56935905b95eaf24869002c294edc"
},
"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": "6319bea4f5382210b2b4188809fd64b8706270c7"
},
"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": "1901e13ba6d3dd5fee3dcc6a689a85c57bfe286e"
},
"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": "2a3c7ccccc015137c9eb294e7eb2753737fb2b2d"
},
"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": "863683e0450bbfbd28923e5819e6b1dab1f1fad6"
},
"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": "33446f9e6fb7e4d6153d70be5830db54766fe29f"
},
"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": "4732ff83cb573dca28e0bdf40ed6adc67acc1e95"
},
"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": "b8e1d921f68cd8754ceb91962d98592a0f29f999"
},
"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": "3ccb96008dba438ab655fe6c963056fb91a7ef1e"
},
"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": "19146ecdace391d8d185770a7bfc54630b10e3d4"
},
"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": "1d4326abaf85a445aac48860ae114c9064dacae7"
},
"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": "67377ffc2d781f0e66a740be1cc5ae98b35999fb"
},
"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": "6da6f444dc061ca4d04b93d8da2809f1a78e1b00"
},
"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": "0d4caee8ecdd4d2eb02fe50004b336bfc92875d2"
},
"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": "0aba5067b13bcb73ca84410e2bac4ab8ad779188"
},
"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": "3253a1812ec2712e146b31b5c91c2685e477dc05"
},
"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": "07e57f2d29f55c9340a3e32b6bbbbf719a70cdc4"
},
"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": "26ee8bc1c2fb5dada2e64597d2e2845f861bedc4"
},
"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": "b26705f0928224e8b3a87b3bf70ac3f45168c4c5"
},
"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": "938be7c9cd7726f58db1641f4dff5f3162da90ac"
},
"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": "22962edbb5280103883a3a6b239e9cac575d65ce"
},
"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": "48c65a854d39f429c1c73f736b4959bc337960cd"
},
"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": "d6fe7b9e50af23498b5261b31edbe29be0cbb2e1"
},
"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": "a4f5ec7086e319f1ec35328e180e49c3ce1d7adb"
}
} \ 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..0635d17bed 100644
--- a/browser/modules/BrowserUsageTelemetry.sys.mjs
+++ b/browser/modules/BrowserUsageTelemetry.sys.mjs
@@ -160,6 +160,11 @@ const PLACES_OPEN_COMMANDS = [
"placesCmd_open:tab",
];
+// How long of a delay between events means the start of a new flow?
+// Used by Browser UI Interaction event instrumentation.
+// Default: 5min.
+const FLOW_IDLE_TIME = 5 * 60 * 1000;
+
function telemetryId(widgetId, obscureAddons = true) {
// Add-on IDs need to be obscured.
function addonId(id) {
@@ -872,6 +877,7 @@ export let BrowserUsageTelemetry = {
let source = this._getWidgetContainer(node);
if (item && source) {
+ this.recordInteractionEvent(item, source);
let scalar = `browser.ui.interaction.${source.replace(/-/g, "_")}`;
Services.telemetry.keyedScalarAdd(scalar, telemetryId(item), 1);
if (SET_USAGECOUNT_PREF_BUTTONS.includes(item)) {
@@ -889,6 +895,7 @@ export let BrowserUsageTelemetry = {
node.closest("menupopup")?.triggerNode
);
if (triggerContainer) {
+ this.recordInteractionEvent(item, contextMenu);
let scalar = `browser.ui.interaction.${contextMenu.replace(/-/g, "_")}`;
Services.telemetry.keyedScalarAdd(
scalar,
@@ -899,6 +906,34 @@ export let BrowserUsageTelemetry = {
}
},
+ _flowId: null,
+ _flowIdTS: 0,
+
+ recordInteractionEvent(widgetId, source) {
+ // A note on clocks. Cu.now() is monotonic, but its behaviour across
+ // computer sleeps is different per platform.
+ // We're okay with this for flows because we're looking at idle times
+ // on the order of minutes and within the same machine, so the weirdest
+ // thing we may expect is a flow that accidentally continues across a
+ // sleep. Until we have evidence that this is common, we're in the clear.
+ if (!this._flowId || this._flowIdTS + FLOW_IDLE_TIME < Cu.now()) {
+ // We submit the ping full o' events on every new flow,
+ // including at startup.
+ GleanPings.prototypeNoCodeEvents.submit();
+ // We use a GUID here because we need to identify events in a flow
+ // out of all events from all flows across all clients.
+ this._flowId = Services.uuid.generateUUID();
+ }
+ this._flowIdTS = Cu.now();
+
+ const extra = {
+ source,
+ widgetId: telemetryId(widgetId),
+ flowId: this._flowId,
+ };
+ Glean.browserUsage.interaction.record(extra);
+ },
+
/**
* Listens for UI interactions in the window.
*/
@@ -1069,7 +1104,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..f6cbcfcd88 100644
--- a/browser/modules/ExtensionsUI.sys.mjs
+++ b/browser/modules/ExtensionsUI.sys.mjs
@@ -119,12 +119,12 @@ export var ExtensionsUI = {
showAddonsManager(tabbrowser, strings, icon) {
let global = tabbrowser.selectedBrowser.ownerGlobal;
- return global
- .BrowserOpenAddonsMgr("addons://list/extension")
- .then(aomWin => {
+ return global.BrowserAddonUI.openAddonsMgr("addons://list/extension").then(
+ aomWin => {
let aomBrowser = aomWin.docShell.chromeEventHandler;
return this.showPermissionsPrompt(aomBrowser, strings, icon);
- });
+ }
+ );
},
showSideloaded(tabbrowser, addon) {
@@ -134,7 +134,7 @@ export var ExtensionsUI = {
let strings = this._buildStrings({
addon,
- permissions: addon.userPermissions,
+ permissions: addon.installPermissions,
type: "sideload",
});
@@ -185,7 +185,7 @@ export var ExtensionsUI = {
);
},
- observe(subject, topic, data) {
+ observe(subject, topic) {
if (topic == "webextension-permission-prompt") {
let { target, info } = subject.wrappedJSObject;
@@ -339,8 +339,6 @@ export var ExtensionsUI = {
async showPermissionsPrompt(target, strings, icon) {
let { browser, window } = getTabBrowser(target);
- await window.ensureCustomElements("moz-support-link");
-
// Wait for any pending prompts to complete before showing the next one.
let pending;
while ((pending = this.pendingNotifications.get(browser))) {
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/FirefoxBridgeExtensionUtils.sys.mjs b/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs
index 7b0094205d..e1222db6e0 100644
--- a/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs
+++ b/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs
@@ -56,6 +56,10 @@ export const FirefoxBridgeExtensionUtils = {
* In Firefox 122, we enabled the firefox and firefox-private protocols.
* We switched over to using firefox-bridge and firefox-private-bridge,
*
+ * In Firefox 126, we deleted the above firefox-bridge and
+ * firefox-private-bridge protocols in favor of using native
+ * messaging so we are only keeping the deletion code.
+ *
* but we want to clean up the use of the other protocols.
*
* deleteBridgeProtocolRegistryEntryHelper handles everything outside of the logic needed for
@@ -66,7 +70,15 @@ export const FirefoxBridgeExtensionUtils = {
* them with. If the entries are changed in any way, it is assumed that the user
* mucked with them manually and knows what they are doing.
*/
+
+ PUBLIC_PROTOCOL: "firefox-bridge",
+ PRIVATE_PROTOCOL: "firefox-private-bridge",
+ OLD_PUBLIC_PROTOCOL: "firefox",
+ OLD_PRIVATE_PROTOCOL: "firefox-private",
+
maybeDeleteBridgeProtocolRegistryEntries(
+ publicProtocol = this.PUBLIC_PROTOCOL,
+ privateProtocol = this.PRIVATE_PROTOCOL,
deleteBridgeProtocolRegistryEntryHelper = new DeleteBridgeProtocolRegistryEntryHelperImplementation()
) {
try {
@@ -110,9 +122,9 @@ export const FirefoxBridgeExtensionUtils = {
}
};
- maybeDeleteRegistryKey("firefox", `\"${path}\" -osint -url \"%1\"`);
+ maybeDeleteRegistryKey(publicProtocol, `\"${path}\" -osint -url \"%1\"`);
maybeDeleteRegistryKey(
- "firefox-private",
+ privateProtocol,
`\"${path}\" -osint -private-window \"%1\"`
);
} catch (err) {
@@ -122,111 +134,6 @@ export const FirefoxBridgeExtensionUtils = {
}
},
- /**
- * Registers the firefox-bridge and firefox-private-bridge protocols
- * on the Windows platform.
- */
- maybeRegisterFirefoxBridgeProtocols() {
- const FIREFOX_BRIDGE_HANDLER_NAME = "firefox-bridge";
- const FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME = "firefox-private-bridge";
- const path = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
- let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
- Ci.nsIWindowsRegKey
- );
- try {
- wrk.open(wrk.ROOT_KEY_CLASSES_ROOT, "", wrk.ACCESS_READ);
- let FxSet = wrk.hasChild(FIREFOX_BRIDGE_HANDLER_NAME);
- let FxPrivateSet = wrk.hasChild(FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME);
- wrk.close();
- if (FxSet && FxPrivateSet) {
- return;
- }
- wrk.open(wrk.ROOT_KEY_CURRENT_USER, "Software\\Classes", wrk.ACCESS_ALL);
- const maybeUpdateRegistry = (isSetAlready, handler, protocolName) => {
- if (isSetAlready) {
- return;
- }
- let FxKey = wrk.createChild(handler, wrk.ACCESS_ALL);
- try {
- // Write URL protocol key
- FxKey.writeStringValue("", protocolName);
- FxKey.writeStringValue("URL Protocol", "");
- FxKey.close();
- // Write defaultIcon key
- FxKey.create(
- FxKey.ROOT_KEY_CURRENT_USER,
- "Software\\Classes\\" + handler + "\\DefaultIcon",
- FxKey.ACCESS_ALL
- );
- FxKey.open(
- FxKey.ROOT_KEY_CURRENT_USER,
- "Software\\Classes\\" + handler + "\\DefaultIcon",
- FxKey.ACCESS_ALL
- );
- FxKey.writeStringValue("", `\"${path}\",1`);
- FxKey.close();
- // Write shell\\open\\command key
- FxKey.create(
- FxKey.ROOT_KEY_CURRENT_USER,
- "Software\\Classes\\" + handler + "\\shell",
- FxKey.ACCESS_ALL
- );
- FxKey.create(
- FxKey.ROOT_KEY_CURRENT_USER,
- "Software\\Classes\\" + handler + "\\shell\\open",
- FxKey.ACCESS_ALL
- );
- FxKey.create(
- FxKey.ROOT_KEY_CURRENT_USER,
- "Software\\Classes\\" + handler + "\\shell\\open\\command",
- FxKey.ACCESS_ALL
- );
- FxKey.open(
- FxKey.ROOT_KEY_CURRENT_USER,
- "Software\\Classes\\" + handler + "\\shell\\open\\command",
- FxKey.ACCESS_ALL
- );
- if (handler == FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME) {
- FxKey.writeStringValue(
- "",
- `\"${path}\" -osint -private-window \"%1\"`
- );
- } else {
- FxKey.writeStringValue("", `\"${path}\" -osint -url \"%1\"`);
- }
- } catch (ex) {
- console.error(ex);
- } finally {
- FxKey.close();
- }
- };
-
- try {
- maybeUpdateRegistry(
- FxSet,
- FIREFOX_BRIDGE_HANDLER_NAME,
- "URL:Firefox Bridge Protocol"
- );
- } catch (ex) {
- console.error(ex);
- }
-
- try {
- maybeUpdateRegistry(
- FxPrivateSet,
- FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME,
- "URL:Firefox Private Bridge Protocol"
- );
- } catch (ex) {
- console.error(ex);
- }
- } catch (ex) {
- console.error(ex);
- } finally {
- wrk.close();
- }
- },
-
getNativeMessagingHostId() {
let nativeMessagingHostId = "org.mozilla.firefox_bridge_nmh";
if (AppConstants.NIGHTLY_BUILD) {
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/metrics.yaml b/browser/modules/metrics.yaml
index ac4e0c6ef9..a4fdba875d 100644
--- a/browser/modules/metrics.yaml
+++ b/browser/modules/metrics.yaml
@@ -118,3 +118,49 @@ performance.interaction:
- mconley@mozilla.com
- perf-telemetry-alerts@mozilla.com
expires: never
+
+browser.usage:
+ interaction:
+ type: event
+ description: >
+ The user interacted with something in the Firefox Desktop frontend.
+ Could be via mouse or keyboard, could be a command or a UI element.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1889111
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1889111
+ expires: 132
+ data_sensitivity: [interaction]
+ notification_emails:
+ - chutten@mozilla.com
+ extra_keys:
+ flow_id:
+ type: string
+ description: >
+ An UUIDv4 used to group interaction events together under the
+ assumption that they're part of the same user activity.
+ See BrowserUsageTelemetry's FLOW_IDLE_TIME for details.
+ source:
+ type: string
+ description: >
+ The source of the interaction. Usually a UI section
+ (like `bookmarks_bar` or `content_context`), but can also be an input
+ method (like `keyboard`).
+ The full list of supported `source`s can be found in
+ `BrowserUsageTelemetry`'s `BROWSER_UI_CONTAINER_IDS. Plus `keyboard`
+ and panes from `about:preferences` listed in `PREFERENCES_PANES`.
+ See `_getWidgetContainer` for details.
+ widget_id:
+ type: string
+ description: >
+ The item interacted with.
+ Usually the `id` of the DOM Node that the user used,
+ sometimes the `id` of the parent or ancestor Node instead.
+ This node is then conjugated by obscuring any addon id in it
+ (turning it to the string `addonX` where `X` is a number stable
+ within a browsing session) and then replacing any underscore with a
+ hyphen.
+ See `BrowserUsageTelemetry#_getWidgetID` and `telemetryId`.
+ e.g. "Browser:Reload", "key-newNavigatorTab", "PanelUI-Bookmarks".
+ send_in_pings:
+ - prototype-no-code-events
diff --git a/browser/modules/pings.yaml b/browser/modules/pings.yaml
new file mode 100644
index 0000000000..0bc4d2227f
--- /dev/null
+++ b/browser/modules/pings.yaml
@@ -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/.
+
+---
+$schema: moz://mozilla.org/schemas/glean/pings/2-0-0
+
+prototype-no-code-events:
+ description: |
+ **Prototype-only ping not for general use!**
+ Transport for no-code Firefox Desktop frontend instrumentation,
+ should mostly contain no-code events in browser.ui.* categories.
+ Submitted whenever the next flow of events begins (including startup).
+ include_client_id: true
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1889111
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1889111
+ notification_emails:
+ - chutten@mozilla.com
+ - tlong@mozilla.com
+ enabled: false # To be enabled by Server Knobs for selected populations.
diff --git a/browser/modules/test/browser/browser.toml b/browser/modules/test/browser/browser.toml
index 21b3cdf18c..82611ed4b2 100644
--- a/browser/modules/test/browser/browser.toml
+++ b/browser/modules/test/browser/browser.toml
@@ -36,6 +36,10 @@ support-files = [
"../../../base/content/test/tabs/file_mediaPlayback.html",
"../../../base/content/test/general/audio.ogg",
]
+skip-if = [
+ "os == 'linux' && os_version == '18.04' && asan", # Bug 1781868
+ "os == 'linux' && os_version == '18.04' && tsan", # Bug 1781868
+]
["browser_Telemetry_numberOfSiteOrigins.js"]
support-files = ["contain_iframe.html"]
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..963dc2d4b4 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
@@ -183,7 +184,7 @@ add_task(async function waitForScriptTest() {
});
// Click the "Close" button this time, we shouldn't get a callback at all.
- notification.currentNotification.closeButtonEl.click();
+ notification.currentNotification.closeButton.click();
// send another hang pulse, we should not get a notification here
Services.obs.notifyObservers(hangReport, "process-hang-report");
diff --git a/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js b/browser/modules/test/browser/browser_UnsubmittedCrashHandler.js
index 4305d7f7df..d105e8374e 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,
@@ -292,7 +292,7 @@ add_task(async function test_other_ignored() {
Assert.ok(notification, "There should be a notification");
// Dismiss notification, creating the .dmp.ignore file
- notification.closeButtonEl.click();
+ notification.closeButton.click();
gNotificationBox.removeNotification(notification, true);
await waitForIgnoredReports(toIgnore);
@@ -525,7 +525,7 @@ add_task(async function test_can_ignore() {
Assert.ok(notification, "There should be a notification");
// Dismiss the notification by clicking on the "X" button.
- notification.closeButtonEl.click();
+ notification.closeButton.click();
// We'll not wait for the notification to finish its transition -
// we'll just remove it right away.
gNotificationBox.removeNotification(notification, true);
@@ -599,7 +599,7 @@ add_task(async function test_shutdown_while_not_showing() {
Assert.ok(notification, "There should be a notification");
// Dismiss the notification by clicking on the "X" button.
- notification.closeButtonEl.click();
+ notification.closeButton.click();
// We'll not wait for the notification to finish its transition -
// we'll just remove it right away.
gNotificationBox.removeNotification(notification, true);
diff --git a/browser/modules/test/browser/browser_UsageTelemetry_interaction.js b/browser/modules/test/browser/browser_UsageTelemetry_interaction.js
index 2bc60d9697..56a7f530ad 100644
--- a/browser/modules/test/browser/browser_UsageTelemetry_interaction.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_interaction.js
@@ -33,6 +33,8 @@ const AREAS = [
// keys in the scalars. Also runs keyed scalar checks against non-area types
// passed in through expectedOther.
function assertInteractionScalars(expectedAreas, expectedOther = {}) {
+ // Every time this checks Scalars, it clears them. So clear FOG too.
+ Services.fog.testResetFOG();
let processScalars =
Services.telemetry.getSnapshotForKeyedScalars("main", true)?.parent ?? {};
@@ -83,6 +85,7 @@ add_task(async function toolbarButtons() {
});
Services.telemetry.getSnapshotForKeyedScalars("main", true);
+ Services.fog.testResetFOG();
let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
let tabClose = BrowserTestUtils.waitForTabClosing(newTab);
@@ -164,6 +167,22 @@ add_task(async function toolbarButtons() {
click(customButton);
+ let events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["nav-bar", "stop-reload-button"],
+ ["nav-bar", "back-button"],
+ ["nav-bar", "back-button"],
+ ["all-tabs-panel-entrypoint", "alltabs-button"],
+ ["tabs-bar", "alltabs-button"],
+ ["tabs-bar", "tab-close-button"],
+ ["bookmarks-bar", "bookmark-item"],
+ ["nav-bar", "12foo"],
+ ],
+ events
+ );
assertInteractionScalars(
{
nav_bar: {
@@ -192,6 +211,7 @@ add_task(async function toolbarButtons() {
add_task(async function contextMenu() {
await BrowserTestUtils.withNewTab("https://example.com", async browser => {
Services.telemetry.getSnapshotForKeyedScalars("main", true);
+ Services.fog.testResetFOG();
let tab = gBrowser.getTabForBrowser(browser);
let context = elem("tabContextMenu");
@@ -207,6 +227,16 @@ add_task(async function contextMenu() {
context.activateItem(document.getElementById("context_toggleMuteTab"));
await hidden;
+ let events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["tabs-context", "context-toggleMuteTab"],
+ ["tabs-context-entrypoint", "context-toggleMuteTab"],
+ ],
+ events
+ );
assertInteractionScalars({
tabs_context: {
"context-toggleMuteTab": 1,
@@ -233,6 +263,16 @@ add_task(async function contextMenu() {
);
await hidden;
+ events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["tabs-context", "toolbar-context-selectAllTabs"],
+ ["tabs-context-entrypoint", "toolbar-context-selectAllTabs"],
+ ],
+ events
+ );
assertInteractionScalars({
tabs_context: {
"toolbar-context-selectAllTabs": 1,
@@ -316,8 +356,9 @@ 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);
+ Services.fog.testResetFOG();
let shown = BrowserTestUtils.waitForEvent(
elem("appMenu-popup"),
@@ -339,17 +380,30 @@ add_task(async function appMenu() {
nav_bar: {
"PanelUI-menu-button": 1,
},
- app_menu: {},
+ app_menu: {
+ [findButtonID]: 1,
+ },
};
- expectedScalars.app_menu[findButtonID] = 1;
+
+ let events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["nav-bar", "PanelUI-menu-button"],
+ ["app-menu", findButtonID],
+ ],
+ events
+ );
assertInteractionScalars(expectedScalars);
});
});
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);
+ Services.fog.testResetFOG();
let shown = BrowserTestUtils.waitForEvent(
elem("appMenu-popup"),
@@ -381,6 +435,17 @@ add_task(async function devtools() {
BrowserTestUtils.removeTab(tab);
// Note that item ID's have '_' converted to '-'.
+ let events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["nav-bar", "PanelUI-menu-button"],
+ ["app-menu", "appMenu-more-button2"],
+ ["app-menu", "key-viewSource"],
+ ],
+ events
+ );
assertInteractionScalars({
nav_bar: {
"PanelUI-menu-button": 1,
@@ -398,6 +463,7 @@ add_task(async function webextension() {
await BrowserTestUtils.withNewTab("https://example.com", async browser => {
Services.telemetry.getSnapshotForKeyedScalars("main", true);
+ Services.fog.testResetFOG();
function background() {
browser.commands.onCommand.addListener(() => {
@@ -470,6 +536,11 @@ add_task(async function webextension() {
// As the first add-on interacted with this should show up as `addon0`.
click("random_addon_example_com-browser-action");
+ let events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["nav-bar", "addon0"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
nav_bar: {
addon0: 1,
@@ -482,6 +553,11 @@ add_task(async function webextension() {
);
click("pageAction-urlbar-random_addon_example_com");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["pageaction-urlbar", "addon0"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
pageaction_urlbar: {
addon0: 1,
@@ -490,6 +566,11 @@ add_task(async function webextension() {
EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
await extension.awaitMessage("oncommand");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["keyboard", "addon0"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
keyboard: {
addon0: 1,
@@ -498,6 +579,11 @@ add_task(async function webextension() {
EventUtils.synthesizeKey("q", { altKey: true, shiftKey: true });
await extension.awaitMessage("sidebar-opened");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["keyboard", "addon0"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
keyboard: {
addon0: 1,
@@ -537,6 +623,11 @@ add_task(async function webextension() {
// A second extension should be `addon1`.
click("random_addon2_example_com-browser-action");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["nav-bar", "addon1"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
nav_bar: {
addon1: 1,
@@ -549,6 +640,11 @@ add_task(async function webextension() {
);
click("pageAction-urlbar-random_addon2_example_com");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["pageaction-urlbar", "addon1"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
pageaction_urlbar: {
addon1: 1,
@@ -557,6 +653,11 @@ add_task(async function webextension() {
EventUtils.synthesizeKey("9", { altKey: true, shiftKey: true });
await extension2.awaitMessage("oncommand");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["keyboard", "addon1"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
keyboard: {
addon1: 1,
@@ -565,6 +666,11 @@ add_task(async function webextension() {
// The first should have retained its ID.
click("random_addon_example_com-browser-action");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["nav-bar", "addon0"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
nav_bar: {
addon0: 1,
@@ -573,6 +679,11 @@ add_task(async function webextension() {
EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
await extension.awaitMessage("oncommand");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["keyboard", "addon0"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
keyboard: {
addon0: 1,
@@ -580,6 +691,11 @@ add_task(async function webextension() {
});
click("pageAction-urlbar-random_addon_example_com");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["pageaction-urlbar", "addon0"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
pageaction_urlbar: {
addon0: 1,
@@ -590,11 +706,19 @@ add_task(async function webextension() {
// Clear the last opened ID so if this test runs again the sidebar won't
// automatically open when the extension is installed.
- window.SidebarUI.lastOpenedId = null;
+ window.SidebarController.lastOpenedId = null;
// The second should retain its ID.
click("random_addon2_example_com-browser-action");
click("random_addon2_example_com-browser-action");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [
+ ["nav-bar", "addon1"],
+ ["nav-bar", "addon1"],
+ ],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
nav_bar: {
addon1: 2,
@@ -602,6 +726,11 @@ add_task(async function webextension() {
});
click("pageAction-urlbar-random_addon2_example_com");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["pageaction-urlbar", "addon1"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
pageaction_urlbar: {
addon1: 1,
@@ -610,6 +739,11 @@ add_task(async function webextension() {
EventUtils.synthesizeKey("9", { altKey: true, shiftKey: true });
await extension2.awaitMessage("oncommand");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["keyboard", "addon1"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
keyboard: {
addon1: 1,
@@ -643,6 +777,11 @@ add_task(async function webextension() {
await shown;
click("random_addon3_example_com-browser-action");
+ events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["unified-extensions-area", "addon2"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
unified_extensions_area: {
addon2: 1,
@@ -667,8 +806,9 @@ 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);
+ Services.fog.testResetFOG();
CustomizableUI.setToolbarVisibility("toolbar-menubar", true);
@@ -686,6 +826,11 @@ add_task(async function mainMenu() {
click("menu_selectAll");
await hidden;
+ let events = Glean.browserUsage.interaction.testGetValue();
+ Assert.deepEqual(
+ [["menu-bar", "menu-selectAll"]],
+ events.map(e => [e.extra.source, e.extra.widget_id])
+ );
assertInteractionScalars({
menu_bar: {
// Note that the _ is replaced with - for telemetry identifiers.
@@ -702,10 +847,11 @@ 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);
+ Services.fog.testResetFOG();
await BrowserTestUtils.synthesizeMouseAtCenter(
"#browserRestoreSession",
@@ -742,6 +888,16 @@ add_task(async function preferences() {
await onLearnMoreOpened;
gBrowser.removeCurrentTab();
+ let events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["preferences_paneGeneral", "browserRestoreSession"],
+ ["preferences_panePrivacy", "contentBlockingLearnMore"],
+ ],
+ events
+ );
assertInteractionScalars({
preferences_paneGeneral: {
browserRestoreSession: 1,
@@ -778,7 +934,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"
@@ -806,6 +962,17 @@ async function history_appMenu(useContextClick) {
app_menu: { "history-item": 1, "appMenu-history-button": 1 },
};
+ let events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["nav-bar", "PanelUI-menu-button"],
+ ["app-menu", "appMenu-history-button"],
+ ["app-menu", "history-item"],
+ ],
+ events
+ );
assertInteractionScalars(expectedScalars);
});
}
@@ -819,7 +986,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"
@@ -852,6 +1019,17 @@ async function bookmarks_appMenu(useContextClick) {
app_menu: { "bookmark-item": 1, "appMenu-bookmarks-button": 1 },
};
+ let events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["nav-bar", "PanelUI-menu-button"],
+ ["app-menu", "appMenu-bookmarks-button"],
+ ["app-menu", "bookmark-item"],
+ ],
+ events
+ );
assertInteractionScalars(expectedScalars);
});
}
@@ -865,7 +1043,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();
@@ -893,6 +1071,17 @@ async function bookmarks_library_navbar(useContextClick) {
"appMenu-library-bookmarks-button": 1,
},
};
+ let events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["nav-bar", "library-button"],
+ ["nav-bar", "appMenu-library-bookmarks-button"],
+ ["nav-bar", "bookmark-item"],
+ ],
+ events
+ );
assertInteractionScalars(expectedScalars);
});
@@ -908,7 +1097,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();
@@ -940,6 +1129,17 @@ async function history_library_navbar(useContextClick) {
"appMenu-library-history-button": 1,
},
};
+ let events = Glean.browserUsage.interaction
+ .testGetValue()
+ .map(e => [e.extra.source, e.extra.widget_id]);
+ Assert.deepEqual(
+ [
+ ["nav-bar", "library-button"],
+ ["nav-bar", "appMenu-library-history-button"],
+ ["nav-bar", "history-item"],
+ ],
+ events
+ );
assertInteractionScalars(expectedScalars);
});
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..d4dcd4fad6 100644
--- a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js
+++ b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js
@@ -7,9 +7,10 @@ const { FirefoxBridgeExtensionUtils } = ChromeUtils.importESModule(
"resource:///modules/FirefoxBridgeExtensionUtils.sys.mjs"
);
-const FIREFOX_SHELL_OPEN_COMMAND_PATH = "firefox\\shell\\open\\command";
-const FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH =
- "firefox-private\\shell\\open\\command";
+const OLD_FIREFOX_SHELL_OPEN_COMMAND_PATH = `${FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL}\\shell\\open\\command`;
+const OLD_FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH = `${FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL}\\shell\\open\\command`;
+const FIREFOX_SHELL_OPEN_COMMAND_PATH = `${FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL}\\shell\\open\\command`;
+const FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH = `${FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL}\\shell\\open\\command`;
class StubbedRegistryKey {
#children;
@@ -39,7 +40,7 @@ class StubbedRegistryKey {
this.#children = new Map(this.#originalChildren);
}
- open(accessLevel) {
+ open(_accessLevel) {
this.#openedForRead = true;
}
@@ -145,206 +146,274 @@ class StubbedDeleteBridgeProtocolRegistryEntryHelper {
}
add_task(async function test_DeleteWhenSameFirefoxInstall() {
- const applicationPath = "testPath";
-
- const firefoxEntries = new Map();
- firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`);
-
- const firefoxProtocolRegKey = new StubbedRegistryKey(
- new Map(),
- firefoxEntries
- );
-
- const firefoxPrivateEntries = new Map();
- firefoxPrivateEntries.set(
- "",
- `\"${applicationPath}\" -osint -private-window \"%1\"`
- );
- const firefoxPrivateProtocolRegKey = new StubbedRegistryKey(
- new Map(),
- firefoxPrivateEntries
- );
-
- const children = new Map();
- children.set(FIREFOX_SHELL_OPEN_COMMAND_PATH, firefoxProtocolRegKey);
- children.set(
- FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH,
- firefoxPrivateProtocolRegKey
- );
-
- const registryRootKey = new StubbedRegistryKey(children, new Map());
-
- const stubbedDeleteBridgeProtocolRegistryHelper =
- new StubbedDeleteBridgeProtocolRegistryEntryHelper({
- applicationPath,
- registryRootKey,
- });
-
- FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
- stubbedDeleteBridgeProtocolRegistryHelper
- );
-
- ok(registryRootKey.wasCloseCalled, "Root key closed");
-
- ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened");
- ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed");
- ok(
- registryRootKey.isChildDeleted("firefox"),
- "Firefox protocol registry entry deleted"
- );
-
- ok(
- firefoxPrivateProtocolRegKey.wasOpenedForRead,
- "Firefox private key opened"
- );
- ok(firefoxPrivateProtocolRegKey.wasCloseCalled, "Firefox private key closed");
- ok(
- registryRootKey.isChildDeleted("firefox-private"),
- "Firefox private protocol registry entry deleted"
- );
+ for (let protocols of [
+ [
+ FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL,
+ FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL,
+ OLD_FIREFOX_SHELL_OPEN_COMMAND_PATH,
+ OLD_FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH,
+ ],
+ [
+ FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL,
+ FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL,
+ FIREFOX_SHELL_OPEN_COMMAND_PATH,
+ FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH,
+ ],
+ ]) {
+ let [publicProtocol, privateProtocol, publicPath, privatePath] = protocols;
+ const applicationPath = "testPath";
+
+ const firefoxEntries = new Map();
+ firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`);
+
+ const firefoxProtocolRegKey = new StubbedRegistryKey(
+ new Map(),
+ firefoxEntries
+ );
+
+ const firefoxPrivateEntries = new Map();
+ firefoxPrivateEntries.set(
+ "",
+ `\"${applicationPath}\" -osint -private-window \"%1\"`
+ );
+ const firefoxPrivateProtocolRegKey = new StubbedRegistryKey(
+ new Map(),
+ firefoxPrivateEntries
+ );
+
+ const children = new Map();
+ children.set(publicPath, firefoxProtocolRegKey);
+ children.set(privatePath, firefoxPrivateProtocolRegKey);
+
+ const registryRootKey = new StubbedRegistryKey(children, new Map());
+
+ const stubbedDeleteBridgeProtocolRegistryHelper =
+ new StubbedDeleteBridgeProtocolRegistryEntryHelper({
+ applicationPath,
+ registryRootKey,
+ });
+
+ FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
+ publicProtocol,
+ privateProtocol,
+ stubbedDeleteBridgeProtocolRegistryHelper
+ );
+
+ ok(registryRootKey.wasCloseCalled, "Root key closed");
+
+ ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened");
+ ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed");
+ ok(
+ registryRootKey.isChildDeleted(publicProtocol),
+ "Firefox protocol registry entry deleted"
+ );
+
+ ok(
+ firefoxPrivateProtocolRegKey.wasOpenedForRead,
+ "Firefox private key opened"
+ );
+ ok(
+ firefoxPrivateProtocolRegKey.wasCloseCalled,
+ "Firefox private key closed"
+ );
+ ok(
+ registryRootKey.isChildDeleted(privateProtocol),
+ "Firefox private protocol registry entry deleted"
+ );
+ }
});
add_task(async function test_DeleteWhenDifferentFirefoxInstall() {
- const applicationPath = "testPath";
- const badApplicationPath = "testPath2";
-
- const firefoxEntries = new Map();
- firefoxEntries.set("", `\"${badApplicationPath}\" -osint -url \"%1\"`);
-
- const firefoxProtocolRegKey = new StubbedRegistryKey(
- new Map(),
- firefoxEntries
- );
-
- const firefoxPrivateEntries = new Map();
- firefoxPrivateEntries.set(
- "",
- `\"${badApplicationPath}\" -osint -private-window \"%1\"`
- );
- const firefoxPrivateProtocolRegKey = new StubbedRegistryKey(
- new Map(),
- firefoxPrivateEntries
- );
-
- const children = new Map();
- children.set(FIREFOX_SHELL_OPEN_COMMAND_PATH, firefoxProtocolRegKey);
- children.set(
- FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH,
- firefoxPrivateProtocolRegKey
- );
-
- const registryRootKey = new StubbedRegistryKey(children, new Map());
-
- const stubbedDeleteBridgeProtocolRegistryHelper =
- new StubbedDeleteBridgeProtocolRegistryEntryHelper({
- applicationPath,
- registryRootKey,
- });
-
- FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
- stubbedDeleteBridgeProtocolRegistryHelper
- );
-
- ok(registryRootKey.wasCloseCalled, "Root key closed");
-
- ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened");
- ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed");
- ok(
- !registryRootKey.isChildDeleted("firefox"),
- "Firefox protocol registry entry not deleted"
- );
-
- ok(
- firefoxPrivateProtocolRegKey.wasOpenedForRead,
- "Firefox private key opened"
- );
- ok(firefoxPrivateProtocolRegKey.wasCloseCalled, "Firefox private key closed");
- ok(
- !registryRootKey.isChildDeleted("firefox-private"),
- "Firefox private protocol registry entry not deleted"
- );
+ for (let protocols of [
+ [
+ FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL,
+ FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL,
+ OLD_FIREFOX_SHELL_OPEN_COMMAND_PATH,
+ OLD_FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH,
+ ],
+ [
+ FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL,
+ FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL,
+ FIREFOX_SHELL_OPEN_COMMAND_PATH,
+ FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH,
+ ],
+ ]) {
+ let [publicProtocol, privateProtocol, publicPath, privatePath] = protocols;
+ const applicationPath = "testPath";
+ const badApplicationPath = "testPath2";
+
+ const firefoxEntries = new Map();
+ firefoxEntries.set("", `\"${badApplicationPath}\" -osint -url \"%1\"`);
+
+ const firefoxProtocolRegKey = new StubbedRegistryKey(
+ new Map(),
+ firefoxEntries
+ );
+
+ const firefoxPrivateEntries = new Map();
+ firefoxPrivateEntries.set(
+ "",
+ `\"${badApplicationPath}\" -osint -private-window \"%1\"`
+ );
+ const firefoxPrivateProtocolRegKey = new StubbedRegistryKey(
+ new Map(),
+ firefoxPrivateEntries
+ );
+
+ const children = new Map();
+ children.set(publicPath, firefoxProtocolRegKey);
+ children.set(privatePath, firefoxPrivateProtocolRegKey);
+
+ const registryRootKey = new StubbedRegistryKey(children, new Map());
+
+ const stubbedDeleteBridgeProtocolRegistryHelper =
+ new StubbedDeleteBridgeProtocolRegistryEntryHelper({
+ applicationPath,
+ registryRootKey,
+ });
+
+ FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
+ publicProtocol,
+ privateProtocol,
+ stubbedDeleteBridgeProtocolRegistryHelper
+ );
+
+ ok(registryRootKey.wasCloseCalled, "Root key closed");
+
+ ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened");
+ ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed");
+ ok(
+ !registryRootKey.isChildDeleted(publicProtocol),
+ "Firefox protocol registry entry not deleted"
+ );
+
+ ok(
+ firefoxPrivateProtocolRegKey.wasOpenedForRead,
+ "Firefox private key opened"
+ );
+ ok(
+ firefoxPrivateProtocolRegKey.wasCloseCalled,
+ "Firefox private key closed"
+ );
+ ok(
+ !registryRootKey.isChildDeleted(privateProtocol),
+ "Firefox private protocol registry entry not deleted"
+ );
+ }
});
add_task(async function test_DeleteWhenNoRegistryEntries() {
- const applicationPath = "testPath";
-
- const firefoxPrivateEntries = new Map();
- const firefoxPrivateProtocolRegKey = new StubbedRegistryKey(
- new Map(),
- firefoxPrivateEntries
- );
-
- const children = new Map();
- children.set(
- FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH,
- firefoxPrivateProtocolRegKey
- );
-
- const registryRootKey = new StubbedRegistryKey(children, new Map());
-
- const stubbedDeleteBridgeProtocolRegistryHelper =
- new StubbedDeleteBridgeProtocolRegistryEntryHelper({
- applicationPath,
- registryRootKey,
- });
-
- FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
- stubbedDeleteBridgeProtocolRegistryHelper
- );
-
- ok(registryRootKey.wasCloseCalled, "Root key closed");
-
- ok(
- firefoxPrivateProtocolRegKey.wasOpenedForRead,
- "Firefox private key opened"
- );
- ok(firefoxPrivateProtocolRegKey.wasCloseCalled, "Firefox private key closed");
- ok(
- !registryRootKey.isChildDeleted("firefox"),
- "Firefox protocol registry entry deleted when it shouldn't be"
- );
- ok(
- !registryRootKey.isChildDeleted("firefox-private"),
- "Firefox private protocol registry deleted when it shouldn't be"
- );
+ for (let protocols of [
+ [
+ FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL,
+ FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL,
+ OLD_FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH,
+ ],
+ [
+ FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL,
+ FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL,
+ FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH,
+ ],
+ ]) {
+ let [publicProtocol, privateProtocol, privatePath] = protocols;
+ const applicationPath = "testPath";
+
+ const firefoxPrivateEntries = new Map();
+ const firefoxPrivateProtocolRegKey = new StubbedRegistryKey(
+ new Map(),
+ firefoxPrivateEntries
+ );
+
+ const children = new Map();
+ children.set(privatePath, firefoxPrivateProtocolRegKey);
+
+ const registryRootKey = new StubbedRegistryKey(children, new Map());
+
+ const stubbedDeleteBridgeProtocolRegistryHelper =
+ new StubbedDeleteBridgeProtocolRegistryEntryHelper({
+ applicationPath,
+ registryRootKey,
+ });
+
+ FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
+ publicProtocol,
+ privateProtocol,
+ stubbedDeleteBridgeProtocolRegistryHelper
+ );
+
+ ok(registryRootKey.wasCloseCalled, "Root key closed");
+
+ ok(
+ firefoxPrivateProtocolRegKey.wasOpenedForRead,
+ "Firefox private key opened"
+ );
+ ok(
+ firefoxPrivateProtocolRegKey.wasCloseCalled,
+ "Firefox private key closed"
+ );
+ ok(
+ !registryRootKey.isChildDeleted(publicProtocol),
+ "Firefox protocol registry entry deleted when it shouldn't be"
+ );
+ ok(
+ !registryRootKey.isChildDeleted(privateProtocol),
+ "Firefox private protocol registry deleted when it shouldn't be"
+ );
+ }
});
add_task(async function test_DeleteWhenUnexpectedRegistryEntries() {
- const applicationPath = "testPath";
-
- const firefoxEntries = new Map();
- firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`);
- firefoxEntries.set("extraEntry", "extraValue");
- const firefoxProtocolRegKey = new StubbedRegistryKey(
- new Map(),
- firefoxEntries
- );
-
- const children = new Map();
- children.set(FIREFOX_SHELL_OPEN_COMMAND_PATH, firefoxProtocolRegKey);
-
- const registryRootKey = new StubbedRegistryKey(children, new Map());
-
- const stubbedDeleteBridgeProtocolRegistryHelper =
- new StubbedDeleteBridgeProtocolRegistryEntryHelper({
- applicationPath,
- registryRootKey,
- });
-
- FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
- stubbedDeleteBridgeProtocolRegistryHelper
- );
-
- ok(registryRootKey.wasCloseCalled, "Root key closed");
-
- ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened");
- ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed");
- ok(
- !registryRootKey.isChildDeleted("firefox"),
- "Firefox protocol registry entry deleted when it shouldn't be"
- );
- ok(
- !registryRootKey.isChildDeleted("firefox-private"),
- "Firefox private protocol registry deleted when it shouldn't be"
- );
+ for (let protocols of [
+ [
+ FirefoxBridgeExtensionUtils.OLD_PUBLIC_PROTOCOL,
+ FirefoxBridgeExtensionUtils.OLD_PRIVATE_PROTOCOL,
+ OLD_FIREFOX_SHELL_OPEN_COMMAND_PATH,
+ ],
+ [
+ FirefoxBridgeExtensionUtils.PUBLIC_PROTOCOL,
+ FirefoxBridgeExtensionUtils.PRIVATE_PROTOCOL,
+ FIREFOX_SHELL_OPEN_COMMAND_PATH,
+ ],
+ ]) {
+ let [publicProtocol, privateProtocol, publicPath] = protocols;
+ const applicationPath = "testPath";
+
+ const firefoxEntries = new Map();
+ firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`);
+ firefoxEntries.set("extraEntry", "extraValue");
+ const firefoxProtocolRegKey = new StubbedRegistryKey(
+ new Map(),
+ firefoxEntries
+ );
+
+ const children = new Map();
+ children.set(publicPath, firefoxProtocolRegKey);
+
+ const registryRootKey = new StubbedRegistryKey(children, new Map());
+
+ const stubbedDeleteBridgeProtocolRegistryHelper =
+ new StubbedDeleteBridgeProtocolRegistryEntryHelper({
+ applicationPath,
+ registryRootKey,
+ });
+
+ FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries(
+ publicProtocol,
+ privateProtocol,
+ stubbedDeleteBridgeProtocolRegistryHelper
+ );
+
+ ok(registryRootKey.wasCloseCalled, "Root key closed");
+
+ ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened");
+ ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed");
+ ok(
+ !registryRootKey.isChildDeleted(publicProtocol),
+ "Firefox protocol registry entry deleted when it shouldn't be"
+ );
+ ok(
+ !registryRootKey.isChildDeleted(privateProtocol),
+ "Firefox private protocol registry deleted when it shouldn't be"
+ );
+ }
});
diff --git a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js
index cef550d705..8686871255 100644
--- a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js
+++ b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js
@@ -16,6 +16,30 @@ const { FirefoxBridgeExtensionUtils } = ChromeUtils.importESModule(
const DUAL_BROWSER_EXTENSION_ORIGIN = ["chrome-extension://fake-origin/"];
const NATIVE_MESSAGING_HOST_ID = "org.mozilla.firefox_bridge_test";
+if (AppConstants.platform == "win") {
+ var { MockRegistry } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistry.sys.mjs"
+ );
+}
+
+let registry = null;
+add_setup(() => {
+ if (AppConstants.platform == "win") {
+ registry = new MockRegistry();
+ registerCleanupFunction(() => {
+ registry.shutdown();
+ });
+ }
+});
+
+function resetMockRegistry() {
+ if (AppConstants.platform != "win") {
+ return;
+ }
+ registry.shutdown();
+ registry = new MockRegistry();
+}
+
let dir = FileUtils.getDir("TmpD", ["NativeMessagingHostsTest"]);
dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
@@ -75,6 +99,35 @@ function getExpectedOutput() {
};
}
+function DumpWindowsRegistry() {
+ let key = "";
+ let pathBuffer = [];
+
+ if (AppConstants.platform == "win") {
+ function bufferPrint(line) {
+ let regPath = line.trimStart();
+ if (regPath.includes(":")) {
+ // After trimming white space, keys are formatted as
+ // ": <key> (<value_type>)". We can assume it's only ever
+ // going to be of type REG_SZ for this test.
+ key = regPath.slice(2, regPath.length - " (REG_SZ)".length);
+ } else {
+ pathBuffer.push(regPath);
+ }
+ }
+
+ MockRegistry.dump(
+ MockRegistry.getRoot(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER),
+ "",
+ bufferPrint
+ );
+ } else {
+ Assert.ok(false, "Only windows has a registry!");
+ }
+
+ return [pathBuffer, key];
+}
+
add_task(async function test_maybeWriteManifestFiles() {
await FirefoxBridgeExtensionUtils.maybeWriteManifestFiles(
USER_TEST_PATH,
@@ -196,6 +249,7 @@ add_task(async function test_ensureRegistered() {
appDir.path,
"Mozilla\\Firefox"
);
+ resetMockRegistry();
} else {
throw new Error("Unsupported platform");
}
@@ -219,4 +273,65 @@ add_task(async function test_ensureRegistered() {
let JSONContent = await IOUtils.readUTF8(expectedJSONPath);
await IOUtils.remove(expectedJSONPath);
Assert.equal(JSONContent, expectedOutput);
+
+ // Test that the registry key is written for Windows only
+ if (AppConstants.platform == "win") {
+ let [pathBuffer, key] = DumpWindowsRegistry();
+ Assert.equal(
+ pathBuffer.toString(),
+ [
+ "Software",
+ "Google",
+ "Chrome",
+ "NativeMessagingHosts",
+ nativeHostId,
+ ].toString()
+ );
+ Assert.equal(key, expectedJSONPath);
+ }
+});
+
+add_task(async function test_maybeWriteNativeMessagingRegKeys() {
+ if (AppConstants.platform != "win") {
+ return;
+ }
+ resetMockRegistry();
+ FirefoxBridgeExtensionUtils.maybeWriteNativeMessagingRegKeys(
+ "Test\\Path\\For\\Reg\\Key",
+ binFile.parent.path,
+ NATIVE_MESSAGING_HOST_ID
+ );
+ let [pathBuffer, key] = DumpWindowsRegistry();
+ registry.shutdown();
+ Assert.equal(
+ pathBuffer.toString(),
+ ["Test", "Path", "For", "Reg", "Key", NATIVE_MESSAGING_HOST_ID].toString()
+ );
+ console.log("The key is: " + key);
+ Assert.equal(key, `${binFile.parent.path}\\${NATIVE_MESSAGING_HOST_ID}.json`);
+});
+
+add_task(async function test_maybeWriteNativeMessagingRegKeysIncorrectValue() {
+ if (AppConstants.platform != "win") {
+ return;
+ }
+ resetMockRegistry();
+ registry.setValue(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `Test\\Path\\For\\Reg\\Key\\${NATIVE_MESSAGING_HOST_ID}`,
+ "",
+ "IncorrectValue"
+ );
+ FirefoxBridgeExtensionUtils.maybeWriteNativeMessagingRegKeys(
+ "Test\\Path\\For\\Reg\\Key",
+ binFile.parent.path,
+ NATIVE_MESSAGING_HOST_ID
+ );
+ let [pathBuffer, key] = DumpWindowsRegistry();
+ registry.shutdown();
+ Assert.equal(
+ pathBuffer.toString(),
+ ["Test", "Path", "For", "Reg", "Key", NATIVE_MESSAGING_HOST_ID].toString()
+ );
+ Assert.equal(key, `${binFile.parent.path}\\${NATIVE_MESSAGING_HOST_ID}.json`);
});
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..ae32edae57 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;
-}
+ > treechildren::-moz-tree-cell-text {
+ margin-inline-end: 6px;
+ }
-#placesList > treechildren::-moz-tree-cell(separator) {
- cursor: default;
-}
+ > treechildren::-moz-tree-cell(separator) {
+ cursor: default;
+ }
-#placesList > treechildren::-moz-tree-separator {
- border-top: 1px solid color-mix(in srgb, FieldText 70%, transparent);
- margin: 0 10px;
-}
-
-#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 */
@@ -172,15 +184,21 @@
#placeContent {
appearance: none;
border: none;
-}
-#placeContent > treechildren::-moz-tree-cell,
-#placeContent > treechildren::-moz-tree-column {
- border-inline-end: 1px solid color-mix(in srgb, FieldText 30%, transparent);
-}
+ > treechildren::-moz-tree-cell,
+ > treechildren::-moz-tree-column {
+ border-inline-start: 1px solid color-mix(in srgb, FieldText 30%, transparent);
+ }
+
+ > treechildren::-moz-tree-column(first-column),
+ > treechildren::-moz-tree-cell(first-column) {
+ /* This matches the treecol separator in tree.css */
+ border-inline-start: none;
+ }
-#placeContent > treechildren::-moz-tree-cell(separator) {
- border-color: transparent;
+ > treechildren::-moz-tree-cell(separator) {
+ border-color: transparent;
+ }
}
/**
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..d7a4bafb81 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"][ac-image]:not([ac-image=""]) > .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..9e513830a9 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);
}
}
@@ -518,6 +515,14 @@ menupopup::part(drop-indicator) {
/* End private browsing indicator */
+/* Content analysis indicator */
+
+:root:not([contentanalysisactive]) #content-analysis-indicator {
+ display: none;
+}
+
+/* End content analysis indicator */
+
/* Override theme colors since the picker uses extra colors that
themes cannot set */
#DateTimePickerPanel {
@@ -651,7 +656,7 @@ menupopup::part(drop-indicator) {
float: inline-end;
background-color: transparent;
flex-direction: row-reverse;
- /* Override proton-doorhanger default styles that increase the size of the button */
+ /* Override doorhanger default styles that increase the size of the button */
margin: 0;
}
diff --git a/browser/themes/shared/controlcenter/dashboard.svg b/browser/themes/shared/controlcenter/dashboard.svg
index e2908d9485..ac3d13fd0c 100644
--- a/browser/themes/shared/controlcenter/dashboard.svg
+++ b/browser/themes/shared/controlcenter/dashboard.svg
@@ -1,4 +1,4 @@
<!-- 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 data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" fill-opacity="context-fill-opacity" d="M15 12H4a2 2 0 0 1-2-2V3a1 1 0 0 0-2 0v7a4 4 0 0 0 4 4h11a1 1 0 0 0 0-2z"/><path fill="context-fill" fill-opacity="context-fill-opacity" d="M4 11a1 1 0 0 0 1-1V8a1 1 0 0 0-2 0v2a1 1 0 0 0 1 1zM7 11a1 1 0 0 0 1-1V4a1 1 0 0 0-2 0v6a1 1 0 0 0 1 1zM10 11a1 1 0 0 0 1-1V6a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1zM13 11a1 1 0 0 0 1-1V7a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1z"/></svg>
+<svg data-name="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill light-dark(black, white)" fill-opacity="context-fill-opacity" d="M15 12H4a2 2 0 0 1-2-2V3a1 1 0 0 0-2 0v7a4 4 0 0 0 4 4h11a1 1 0 0 0 0-2z"/><path fill="context-fill light-dark(black, white)" fill-opacity="context-fill-opacity" d="M4 11a1 1 0 0 0 1-1V8a1 1 0 0 0-2 0v2a1 1 0 0 0 1 1zM7 11a1 1 0 0 0 1-1V4a1 1 0 0 0-2 0v6a1 1 0 0 0 1 1zM10 11a1 1 0 0 0 1-1V6a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1zM13 11a1 1 0 0 0 1-1V7a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1z"/></svg>
diff --git a/browser/themes/shared/controlcenter/panel.css b/browser/themes/shared/controlcenter/panel.css
index b8ba6b72e4..6708bc604c 100644
--- a/browser/themes/shared/controlcenter/panel.css
+++ b/browser/themes/shared/controlcenter/panel.css
@@ -642,6 +642,12 @@
#permission-popup-menulist {
padding-inline: 12px 6px;
+ margin: 0;
+
+ &,
+ > menupopup {
+ min-width: 6.5em;
+ }
}
.protections-popup-section-header,
diff --git a/browser/themes/shared/customizableui/customizeMode.css b/browser/themes/shared/customizableui/customizeMode.css
index b3ee1ada6c..d0bc283256 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 {
@@ -95,10 +95,6 @@ toolbarpaletteitem:not([notransition])[place="panel"] {
opacity: 1;
}
-#customization-palette #firefox-view-button[attention] {
- background-position: center bottom 8%;
-}
-
toolbarpaletteitem toolbarbutton[disabled] {
color: inherit !important;
}
diff --git a/browser/themes/shared/customizableui/panelUI-shared.css b/browser/themes/shared/customizableui/panelUI-shared.css
index 51ab66b25a..2236613cf1 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%);
}
@@ -172,7 +172,6 @@ menupopup {
panelview {
flex-direction: column;
- background: var(--arrowpanel-background);
padding: 0;
/* Prevent a scrollbar from appearing while the animation for transitioning from
@@ -696,6 +695,10 @@ toolbarbutton[constrain-size="true"][cui-areatype="panel"] > .toolbarbutton-badg
margin: 0;
}
+#PanelUI-fxa-cta-menu .fxa-cta-button {
+ margin: var(--space-xsmall);
+}
+
.PanelUI-remotetabs-clientcontainer > label[itemtype="client"] {
font-size: 11px;
}
@@ -1150,6 +1153,11 @@ panelview .toolbarbutton-1,
stroke: var(--panel-item-hover-bgcolor);
}
+#appMenu-zoomReset-button2:not([disabled]):hover > .toolbarbutton-text,
+#appMenu-fullscreen-button2:not([disabled]):hover > .toolbarbutton-icon {
+ background-color: var(--panel-item-hover-bgcolor);
+}
+
#appMenu-zoomReset-button2:not([disabled]):active:hover > .toolbarbutton-text,
#appMenu-fullscreen-button2:not([disabled]):active:hover > .toolbarbutton-icon {
background-color: var(--panel-item-active-bgcolor);
@@ -1300,7 +1308,7 @@ panelview .toolbarbutton-1 {
}
}
-.PanelUI-remotetabs-clientcontainer > toolbarbutton[itemtype="tab"],
+.PanelUI-tabitem-container > toolbarbutton[itemtype="tab"],
#PanelUI-historyItems > toolbarbutton {
list-style-image: url("chrome://global/skin/icons/defaultFavicon.svg");
-moz-context-properties: fill;
@@ -1310,7 +1318,7 @@ panelview .toolbarbutton-1 {
#fxa-menu-account-fxa-avatar,
#appMenu-fxa-label > .toolbarbutton-icon,
#PanelUI-containersItems > .subviewbutton > .toolbarbutton-icon,
-.PanelUI-remotetabs-clientcontainer > toolbarbutton[itemtype="tab"] > .toolbarbutton-icon,
+.PanelUI-tabitem-container > toolbarbutton[itemtype="tab"] > .toolbarbutton-icon,
#PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
#PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
#PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
@@ -1322,6 +1330,15 @@ panelview .toolbarbutton-1 {
min-width: 0;
}
+.PanelUI-tabitem-container > toolbarbutton[itemtype="tab"] {
+ flex: 1;
+ min-width: 0;
+}
+
+.remotetabs-close {
+ width: 18px;
+ margin-inline-end: 4px;
+}
#PanelUI-fxa-menu-account-settings-button > .toolbarbutton-icon {
border-radius: 50%;
}
@@ -1649,19 +1666,15 @@ radiogroup:focus-visible > .subviewradio[focused="true"] {
}
}
-/* What's New panel */
-#customizationui-widget-multiview #PanelUI-whatsNew {
- max-width: var(--menu-panel-width);
-}
-
+/* Protections panel info message */
#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 +1682,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;
}
}
@@ -1695,7 +1708,16 @@ radiogroup:focus-visible > .subviewradio[focused="true"] {
margin: 12px 0;
}
- .whatsNew-message-body {
+ .protections-popup-message-title {
+ display: grid;
+ font-size: 1.3em;
+ font-weight: 600;
+ line-height: 1.4em;
+ margin: 14px 0 0;
+ grid-column-start: 1;
+ }
+
+ .protections-popup-message-body {
/* -10px to compensate for the margin on the container. We can't get rid of that
because it helps position the background image. */
margin: 0 calc(-10px + var(--horizontal-padding)) var(--vertical-section-padding);
@@ -1708,129 +1730,6 @@ radiogroup:focus-visible > .subviewradio[focused="true"] {
}
}
-panelview {
- &[mainview] #PanelUI-whatsNew-content {
- height: 43em;
- }
-
- /* Hide the What's New header when the panel is a subview */
- &:not([mainview]) #PanelUI-whatsNew-title {
- display: none;
- }
-}
-
-#PanelUI-whatsNew {
- .panelMenu-toggleWhatsNew-checkbox {
- padding-inline-start: 16px;
- min-height: 41px;
- }
-
- .whatsNew-message {
- cursor: pointer;
- margin: 0;
- padding: 0;
- }
-
- /* The following 2 rules show a 1 pixel line separator between What's New
- * messages while at the same time ensuring that the first message (which has
- * a date header) will not show the separator
- */
- .whatsNew-message-body::before {
- content: "";
- display: block;
- height: 1px;
- width: 104%;
- margin-inline-start: -2%;
- background: var(--panel-separator-color);
- }
-
- .has-icon::before {
- /* the width of the icon + the grid margin */
- width: calc(104% + 40px);
- }
-
- .whatsNew-message-date + .whatsNew-message-body::before {
- display: none;
- }
-
- .whatsNew-message-date {
- font-size: .85em;
- margin: -12px;
- margin-top: 0;
- margin-inline-start: 0;
- padding: 6px 16px;
- background: var(--arrowpanel-dimmed);
- }
-
- .whatsNew-message-body {
- padding: 5px 0 10px;
- margin: 10px 16px;
- text-align: inherit;
- text-decoration: none;
- color: inherit;
- background: none;
- border: none;
- cursor: pointer;
- }
-
- .whatsNew-message-body.has-icon {
- display: grid;
- grid-template-columns: auto 32px;
- grid-template-rows: 0;
- grid-gap: 0 8px;
- }
-
- .whatsNew-message-icon {
- height: 32px;
- width: 32px;
- margin: 14px auto;
- display: grid;
- grid-column-start: 2;
- }
-
- .whatsNew-message-subtitle {
- margin: 2px 0;
- font-size: .8em;
- color: #949494;
- font-weight: normal;
- grid-column-start: 1;
- }
-
- .whatsNew-message-content {
- display: grid;
- margin: 5px 0 10px;
- grid-column-start: 1;
- }
-
- .text-link {
- background: none;
- border: 0;
- color: #45a1ff;
- cursor: pointer;
- font-size: .9em;
- grid-column-start: 1;
-
- &:hover {
- color: #0a84ff;
- text-decoration: underline;
-
- &:active {
- color: #0060df;
- }
- }
- }
-}
-
-#PanelUI-whatsNew .whatsNew-message-title,
-#protections-popup-message .whatsNew-message-title {
- display: grid;
- font-size: 1.3em;
- font-weight: 600;
- line-height: 1.4em;
- margin: 14px 0 0;
- grid-column-start: 1;
-}
-
#customizationui-widget-panel {
/* In the next two rules the panel's width is set according to the
* profiler backdrop image when not opened from the overflow panel. */
@@ -2122,7 +2021,7 @@ panelview {
&.sent-view {
@media not (prefers-contrast) {
- background-color: var(--color-background-success);
+ background-color: var(--background-color-success);
}
> .panel-header {
@@ -2180,3 +2079,9 @@ panelview {
padding: var(--space-xxlarge) 0 var(--space-medium);
gap: var(--space-small);
}
+
+/* ----- Content Analysis indicator panel ----- */
+
+#content-analysis-panel-container {
+ padding: 8px;
+}
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/back.svg b/browser/themes/shared/icons/back.svg
index b52c06b776..c49e6c1ed2 100644
--- a/browser/themes/shared/icons/back.svg
+++ b/browser/themes/shared/icons/back.svg
@@ -2,5 +2,5 @@
- 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" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
- <path d="M14.375 8 3.048 8l4.308-4.308a.626.626 0 0 0-.885-.885L1 8.281l0 .689 5.472 5.473a.623.623 0 0 0 .884 0 .628.628 0 0 0 0-.885L3.048 9.25l11.327 0a.625.625 0 0 0 0-1.25z"/>
+ <path d="M6.69 2.25 1.22 7.72a.75.75 0 0 0 0 1.06l5.47 5.47 1.06-1.061L3.56 9H15V7.5H3.56l4.19-4.19-1.06-1.06z"/>
</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/forward.svg b/browser/themes/shared/icons/forward.svg
index 2eac6f3ed7..8fef9db8f2 100644
--- a/browser/themes/shared/icons/forward.svg
+++ b/browser/themes/shared/icons/forward.svg
@@ -2,5 +2,5 @@
- 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" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
- <path d="m1.625 8 11.327 0-4.308-4.308a.626.626 0 0 1 .885-.885L15 8.281l0 .689-5.472 5.473a.623.623 0 0 1-.884 0 .628.628 0 0 1 0-.885l4.308-4.308-11.327 0a.625.625 0 0 1 0-1.25z"/>
+ <path d="M12.44 9H1V7.5h11.44L8.25 3.31l1.06-1.06 5.47 5.47a.75.75 0 0 1 0 1.06l-5.47 5.47-1.06-1.061L12.44 9z"/>
</svg>
diff --git a/browser/themes/shared/icons/ion.svg b/browser/themes/shared/icons/ion.svg
index 870914287b..d324bae72e 100644
--- a/browser/themes/shared/icons/ion.svg
+++ b/browser/themes/shared/icons/ion.svg
@@ -1,4 +1,4 @@
<!-- 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" viewBox="0 0 16 16"><path fill="context-fill" d="M13.9 9.81a1.23 1.23 0 0 0 0-.17v-.08a5.67 5.67 0 0 0-2.4-3.36 1.17 1.17 0 0 1-.56-.95V3a1 1 0 0 0-1-1H6.06a1 1 0 0 0-1 1v2.25a1.17 1.17 0 0 1-.56 1 5.66 5.66 0 0 0-2.35 3.33v.12a.53.53 0 0 0 0 .11 5.35 5.35 0 0 0-.11 1 5.65 5.65 0 0 0 3.24 5.09 1 1 0 0 0 .44.1h4.57a1 1 0 0 0 .44-.1A5.65 5.65 0 0 0 14 10.83a5.3 5.3 0 0 0-.1-1.02zm-8.27-2a3.18 3.18 0 0 0 1.43-2.6V4h1.88v1.25a3.18 3.18 0 0 0 1.43 2.6 3.68 3.68 0 0 1 1.54 2.24v.22a2.82 2.82 0 0 1-3.68-.59A3.48 3.48 0 0 0 4.56 9a3.76 3.76 0 0 1 1.07-1.15z"></path></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="context-fill light-dark(black, white)" d="M13.9 9.81a1.23 1.23 0 0 0 0-.17v-.08a5.67 5.67 0 0 0-2.4-3.36 1.17 1.17 0 0 1-.56-.95V3a1 1 0 0 0-1-1H6.06a1 1 0 0 0-1 1v2.25a1.17 1.17 0 0 1-.56 1 5.66 5.66 0 0 0-2.35 3.33v.12a.53.53 0 0 0 0 .11 5.35 5.35 0 0 0-.11 1 5.65 5.65 0 0 0 3.24 5.09 1 1 0 0 0 .44.1h4.57a1 1 0 0 0 .44-.1A5.65 5.65 0 0 0 14 10.83a5.3 5.3 0 0 0-.1-1.02zm-8.27-2a3.18 3.18 0 0 0 1.43-2.6V4h1.88v1.25a3.18 3.18 0 0 0 1.43 2.6 3.68 3.68 0 0 1 1.54 2.24v.22a2.82 2.82 0 0 1-3.68-.59A3.48 3.48 0 0 0 4.56 9a3.76 3.76 0 0 1 1.07-1.15z"></path></svg> \ No newline at end of file
diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn
index 14a5efecac..92d59e6f5e 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)
diff --git a/browser/themes/shared/notification-icons.css b/browser/themes/shared/notification-icons.css
index 92466732f6..5c2d783e94 100644
--- a/browser/themes/shared/notification-icons.css
+++ b/browser/themes/shared/notification-icons.css
@@ -11,13 +11,15 @@
fill-opacity: var(--urlbar-icon-fill-opacity);
color: inherit;
border-radius: var(--urlbar-icon-border-radius);
-}
-#notification-popup-box:hover {
- background-color: hsla(0,0%,70%,.2);
-}
-#notification-popup-box:hover:active,
-#notification-popup-box[open] {
- background-color: hsla(0,0%,70%,.3);
+
+ &:hover {
+ background-color: hsla(0,0%,70%,.2);
+ }
+
+ &:hover:active,
+ &[open] {
+ background-color: hsla(0,0%,70%,.3);
+ }
}
.popup-notification-icon,
@@ -26,24 +28,24 @@
fill: currentColor;
}
-.notification-anchor-icon:focus-visible {
- outline: var(--focus-outline);
- outline-offset: var(--focus-outline-inset);
- border-radius: var(--urlbar-icon-border-radius);
-}
-
-.blocked-permission-icon:focus-visible {
- outline: var(--focus-outline);
- outline-offset: calc(var(--urlbar-icon-padding) + var(--focus-outline-inset));
- border-radius: 1px;
-}
-
/* This class can be used alone or in combination with the class defining the
type of icon displayed. This rule must be defined before the others in order
for its list-style-image to be overridden. */
.notification-anchor-icon {
list-style-image: url(chrome://global/skin/icons/info-filled.svg);
padding: var(--urlbar-icon-padding);
+
+ &:focus-visible {
+ outline: var(--focus-outline);
+ outline-offset: var(--focus-outline-inset);
+ border-radius: var(--urlbar-icon-border-radius);
+ }
+}
+
+.blocked-permission-icon:focus-visible {
+ outline: var(--focus-outline);
+ outline-offset: calc(var(--urlbar-icon-padding) + var(--focus-outline-inset));
+ border-radius: 1px;
}
/* INDIVIDUAL NOTIFICATIONS */
@@ -58,68 +60,50 @@
.persistent-storage-icon {
list-style-image: url(chrome://browser/skin/notification-icons/persistent-storage.svg);
-}
-.persistent-storage-icon.blocked-permission-icon {
- list-style-image: url(chrome://browser/skin/notification-icons/persistent-storage-blocked.svg);
+ &.blocked-permission-icon {
+ list-style-image: url(chrome://browser/skin/notification-icons/persistent-storage-blocked.svg);
+ }
}
.desktop-notification-icon {
list-style-image: url(chrome://browser/skin/notification-icons/desktop-notification.svg);
-}
-.desktop-notification-icon.blocked-permission-icon {
- list-style-image: url(chrome://browser/skin/notification-icons/desktop-notification-blocked.svg);
+ &.blocked-permission-icon {
+ list-style-image: url(chrome://browser/skin/notification-icons/desktop-notification-blocked.svg);
+ }
}
.geo-icon {
list-style-image: url(chrome://browser/skin/notification-icons/geo.svg);
-}
-.geo-icon.blocked-permission-icon {
- list-style-image: url(chrome://browser/skin/notification-icons/geo-blocked.svg);
+ &.blocked-permission-icon {
+ list-style-image: url(chrome://browser/skin/notification-icons/geo-blocked.svg);
+ }
}
.open-protocol-handler-icon {
list-style-image: url(chrome://global/skin/icons/open-in-new.svg);
-}
-.open-protocol-handler-icon:-moz-locale-dir(rtl) {
- transform: scaleX(-1);
+ &:-moz-locale-dir(rtl) {
+ transform: scaleX(-1);
+ }
}
.xr-icon {
list-style-image: url(chrome://browser/skin/notification-icons/xr.svg);
-}
-.xr-icon.blocked-permission-icon {
- list-style-image: url(chrome://browser/skin/notification-icons/xr-blocked.svg);
+ &.blocked-permission-icon {
+ list-style-image: url(chrome://browser/skin/notification-icons/xr-blocked.svg);
+ }
}
.autoplay-media-icon {
list-style-image: url(chrome://browser/skin/notification-icons/autoplay-media.svg);
-}
-.autoplay-media-icon.blocked-permission-icon {
- list-style-image: url(chrome://browser/skin/notification-icons/autoplay-media-blocked.svg);
-}
-
-.storage-access-notification-content {
- color: var(--panel-disabled-color);
- font-style: italic;
- margin-top: 15px;
-}
-
-.storage-access-notification-content .text-link {
- color: LinkText;
-}
-
-.storage-access-notification-content .text-link:hover {
- text-decoration: underline;
-}
-
-#storage-access-notification .popup-notification-body-container {
- padding: 20px;
+ &.blocked-permission-icon {
+ list-style-image: url(chrome://browser/skin/notification-icons/autoplay-media-blocked.svg);
+ }
}
.indexedDB-icon {
@@ -128,59 +112,59 @@
#password-notification-icon {
list-style-image: url(chrome://browser/skin/login.svg);
-}
-#password-notification-icon[extraAttr="attention"] {
- fill: var(--toolbarbutton-icon-fill-attention);
- fill-opacity: 1;
+ &[extraAttr="attention"] {
+ fill: var(--toolbarbutton-icon-fill-attention);
+ fill-opacity: 1;
+ }
}
.camera-icon {
list-style-image: url(chrome://browser/skin/notification-icons/camera.svg);
-}
-.camera-icon.in-use {
- list-style-image: url(chrome://browser/skin/notification-icons/camera.svg);
-}
+ &.in-use {
+ list-style-image: url(chrome://browser/skin/notification-icons/camera.svg);
+ }
-.camera-icon.blocked-permission-icon {
- list-style-image: url(chrome://browser/skin/notification-icons/camera-blocked.svg);
+ &.blocked-permission-icon {
+ list-style-image: url(chrome://browser/skin/notification-icons/camera-blocked.svg);
+ }
}
.microphone-icon {
list-style-image: url(chrome://browser/skin/notification-icons/microphone.svg);
-}
-.microphone-icon.in-use {
- list-style-image: url(chrome://browser/skin/notification-icons/microphone.svg);
-}
+ &.in-use {
+ list-style-image: url(chrome://browser/skin/notification-icons/microphone.svg);
+ }
-.microphone-icon.blocked-permission-icon {
- list-style-image: url(chrome://browser/skin/notification-icons/microphone-blocked.svg);
-}
+ &.blocked-permission-icon {
+ list-style-image: url(chrome://browser/skin/notification-icons/microphone-blocked.svg);
+ }
-.popup-notification-icon.microphone-icon {
- list-style-image: url(chrome://browser/skin/notification-icons/microphone.svg);
+ &.popup-notification-icon {
+ list-style-image: url(chrome://browser/skin/notification-icons/microphone.svg);
+ }
}
+
.screen-icon {
list-style-image: url(chrome://browser/skin/notification-icons/screen.svg);
-}
-.screen-icon.in-use {
- list-style-image: url(chrome://browser/skin/notification-icons/screen.svg);
-}
+ &.in-use {
+ list-style-image: url(chrome://browser/skin/notification-icons/screen.svg);
+ }
-.screen-icon.blocked-permission-icon {
- list-style-image: url(chrome://browser/skin/notification-icons/screen-blocked.svg);
+ &.blocked-permission-icon {
+ list-style-image: url(chrome://browser/skin/notification-icons/screen-blocked.svg);
+ }
}
.speaker-icon {
list-style-image: url(chrome://browser/skin/notification-icons/speaker.svg);
}
-.midi-icon,
-.midi-sysex-icon {
+.midi-icon {
list-style-image: url(chrome://browser/skin/notification-icons/midi.svg);
}
@@ -202,16 +186,8 @@
list-style-image: url(chrome://browser/skin/login.svg);
}
-#permission-popup-menulist {
- margin-inline-end: 0;
-}
-
-#webRTC-preview:not([hidden]) {
- flex-direction: column;
-}
-
#webRTC-previewVideo {
- border-radius: 4px;
+ border-radius: var(--border-radius-small);
border: 1px solid var(--panel-separator-color);
min-width: 0;
min-height: 10em;
@@ -221,17 +197,14 @@
#webRTC-all-windows-shared,
#webRTC-previewWarning {
font-size: 0.8em;
-}
-@media not (prefers-contrast) {
- #webRTC-all-windows-shared,
- #webRTC-previewWarning {
+ @media not (prefers-contrast) {
opacity: 0.6;
}
}
#webRTC-previewWarning {
- margin-block-start: 14px;
+ margin-block-start: var(--space-large);
}
/**
@@ -244,14 +217,6 @@
font-size: 0.75em;
}
-#webRTC-previewWarningBox:-moz-locale-dir(rtl) {
- background-position: calc(100% - .75em) .75em;
-}
-
-#webRTC-previewWarning > .text-link {
- margin-inline-start: 0;
-}
-
/* This icon has a block sign in it, so we don't need a blocked version. */
.popup-icon {
list-style-image: url("chrome://browser/skin/notification-icons/popup.svg");
@@ -267,11 +232,6 @@
transform: translateY(1px);
}
-#permission-popup-menulist,
-#permission-popup-menulist > menupopup {
- min-width: 6.5em;
-}
-
/* EME */
.drm-icon {
@@ -294,13 +254,14 @@
.install-icon {
list-style-image: url(chrome://mozapps/skin/extensions/extension.svg);
-}
-.install-icon.blocked-permission-icon {
- list-style-image: url(chrome://browser/skin/addons/addon-install-blocked.svg);
+ &.blocked-permission-icon {
+ list-style-image: url(chrome://browser/skin/addons/addon-install-blocked.svg);
+ }
}
/* UPDATE */
+
.popup-notification-icon[popupid="update-available"],
.popup-notification-icon[popupid="update-manual"],
.popup-notification-icon[popupid="update-other-instance"],
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..9db5fc45d9 100644
--- a/browser/themes/shared/preferences/preferences.css
+++ b/browser/themes/shared/preferences/preferences.css
@@ -474,18 +474,23 @@ richlistitem[selected] .actionsMenu:focus-visible {
width: 16px;
}
-#homeContentsGroup [data-subcategory] {
+#homeContentsGroup > [data-subcategory] {
margin-top: 14px;
}
-#homeContentsGroup [data-subcategory] .section-checkbox {
+#homeContentsGroup > [data-subcategory] .section-checkbox {
font-weight: var(--font-weight-bold);
}
-#homeContentsGroup [data-subcategory] > vbox menulist {
+#homeContentsGroup > [data-subcategory] > vbox menulist {
margin-block: 0;
}
+/* Align fix for prefs that have a text link in the headline */
+#homeContentsGroup > [data-subcategory] > hbox {
+ align-items: last baseline;
+}
+
#homeContentsGroup .checkbox-label {
margin-inline-end: var(--space-small);
}
@@ -518,7 +523,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);
@@ -583,6 +588,12 @@ a[is="moz-support-link"]:not(.sidebar-footer-link) {
pointer-events: none;
}
+html|label[disabled] {
+ /* This matches XUL checkbox.css - but for HTML labels for HTML inputs we
+ * need to do this ourselves. */
+ opacity: 0.4;
+}
+
#showUpdateHistory {
margin-inline-start: 0;
}
@@ -624,7 +635,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 +1334,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/sanitizeDialog_v2.css b/browser/themes/shared/sanitizeDialog_v2.css
index 40c8aed88b..9f5df748f4 100644
--- a/browser/themes/shared/sanitizeDialog_v2.css
+++ b/browser/themes/shared/sanitizeDialog_v2.css
@@ -9,6 +9,14 @@
white-space: pre-wrap;
}
+/* Add padding to the left to create space between
+ the duration choicemenu (#sanitizeDurationChoice)
+ and the label
+*/
+#sanitizeDurationSuffixLabel {
+ margin-inline-start: var(--space-small);
+}
+
/* Hide the duration dropdown suffix label if it's empty. Otherwise it
takes up a little space, causing the end of the dropdown to not be aligned
with the warning box. */
diff --git a/browser/themes/shared/tabs.css b/browser/themes/shared/tabs.css
index eb92f71e59..9a27b891c6 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);
@@ -60,6 +54,17 @@
color-scheme: unset;
background: var(--tabpanel-background-color);
+ &[pendingpaint] {
+ background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 30px;
+ }
+
+ browser:is([blank], [pendingpaint]) {
+ opacity: 0;
+ }
+
browser[type=content] {
color-scheme: env(-moz-content-preferred-color-scheme);
}
@@ -82,7 +87,17 @@
}
}
+.closing-tabs-spacer {
+ pointer-events: none;
+
+ #tabbrowser-arrowscrollbox:not(:hover) > #tabbrowser-arrowscrollbox-periphery > & {
+ transition: width .15s ease-out;
+ }
+}
+
.tabbrowser-tab {
+ --tab-label-mask-size: 2em;
+
appearance: none;
background-color: transparent;
color: inherit;
@@ -200,72 +215,80 @@
}
}
-@media (prefers-reduced-motion: reduce) {
- .tab-throbber[busy] {
+.tab-throbber {
+ &:not([busy]) {
+ display: none;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
background-image: url("chrome://browser/skin/tabbrowser/hourglass.svg");
background-position: center;
background-repeat: no-repeat;
-moz-context-properties: fill;
fill: currentColor;
opacity: 0.4;
- }
- .tab-throbber[progress] {
- opacity: 0.8;
+ &[progress] {
+ opacity: 0.8;
+ }
}
-}
-
-@media (prefers-reduced-motion: no-preference) {
- :root[sessionrestored] .tab-throbber {
- &[busy] {
- position: relative;
- overflow: hidden;
- &::before {
- content: "";
- position: absolute;
- background-image: url("chrome://browser/skin/tabbrowser/loading.svg");
- background-position: left center;
- background-repeat: no-repeat;
- width: 480px;
- height: 100%;
- animation: tab-throbber-animation 1.05s steps(30) infinite;
- -moz-context-properties: fill;
-
- /* XXX: It would be nice to transition between the "connecting" color and the
- "loading" color (see the `.tab-throbber[progress]::before` rule below);
- however, that currently forces main thread painting. When this is fixed
- (after WebRender lands), we can do something like
- `transition: fill 0.333s, opacity 0.333s;` */
-
- fill: currentColor;
- opacity: 0.7;
+ @media (prefers-reduced-motion: no-preference) {
+ :root[sessionrestored] & {
+ &[busy] {
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: "";
+ position: absolute;
+ background-image: url("chrome://browser/skin/tabbrowser/loading.svg");
+ background-position: left center;
+ background-repeat: no-repeat;
+ width: 480px;
+ height: 100%;
+ animation: tab-throbber-animation 1.05s steps(30) infinite;
+ -moz-context-properties: fill;
+
+ /* XXX: It would be nice to transition between the "connecting" color and the
+ "loading" color (see the `[progress]::before` rule below);
+ however, that currently forces main thread painting. When this is fixed
+ (after WebRender lands), we can do something like
+ `transition: fill 0.333s, opacity 0.333s;` */
+
+ fill: currentColor;
+ opacity: 0.7;
+ }
+
+ &:-moz-locale-dir(rtl)::before {
+ animation-name: tab-throbber-animation-rtl;
+ }
}
- &:-moz-locale-dir(rtl)::before {
- animation-name: tab-throbber-animation-rtl;
+ &[progress]::before {
+ fill: var(--tab-loading-fill);
+ opacity: 1;
}
- }
- &[progress]::before {
- fill: var(--tab-loading-fill);
- opacity: 1;
- }
-
- #TabsToolbar[brighttext] &[progress]:not([selected])::before {
- fill: var(--lwt-tab-loading-fill-inactive, #84c1ff);
+ #TabsToolbar[brighttext] &[progress]:not([selected])::before {
+ fill: var(--lwt-tab-loading-fill-inactive, #84c1ff);
+ }
}
}
+}
- @keyframes tab-throbber-animation {
- 0% { transform: translateX(0); }
- 100% { transform: translateX(-100%); }
- }
+@keyframes tab-throbber-animation {
+ 0% { transform: translateX(0); }
+ 100% { transform: translateX(-100%); }
+}
- @keyframes tab-throbber-animation-rtl {
- 0% { transform: translateX(0); }
- 100% { transform: translateX(100%); }
- }
+@keyframes tab-throbber-animation-rtl {
+ 0% { transform: translateX(0); }
+ 100% { transform: translateX(100%); }
+}
+
+.tab-icon-pending:is(:not([pendingicon]), [busy], [pinned]) {
+ display: none;
}
:root {
@@ -285,6 +308,11 @@
-moz-context-properties: fill;
fill: currentColor;
+ /* Apply crisp rendering for favicons at exactly 2dppx resolution */
+ @media (resolution: 2dppx) {
+ image-rendering: -moz-crisp-edges;
+ }
+
&:not([src]),
&:-moz-broken {
content: url("chrome://global/skin/icons/defaultFavicon.svg");
@@ -294,6 +322,22 @@
animation: var(--tab-sharing-icon-animation);
animation-delay: -1.5s;
}
+
+ &[selected]:not([src], [pinned], [crashed], [pictureinpicture]),
+ &:not([src], [pinned], [crashed], [sharing], [pictureinpicture]),
+ &[busy] {
+ display: none;
+ }
+}
+
+.tab-sharing-icon-overlay,
+.tab-icon-overlay {
+ display: none;
+}
+
+.tab-sharing-icon-overlay[sharing]:not([selected]),
+.tab-icon-overlay:is([soundplaying], [muted], [activemedia-blocked], [crashed]) {
+ display: revert;
}
.tab-sharing-icon-overlay {
@@ -381,8 +425,36 @@
}
}
+.tab-label-container {
+ overflow: hidden;
+
+ #tabbrowser-tabs:not([secondarytext-unsupported]) & {
+ height: 2.7em;
+ }
+
+ &[pinned] {
+ width: 0;
+ }
+
+ &[textoverflow] {
+ &[labeldirection=ltr]:not([pinned]),
+ &:not([labeldirection], [pinned]):-moz-locale-dir(ltr) {
+ direction: ltr;
+ mask-image: linear-gradient(to left, transparent, black var(--tab-label-mask-size));
+ }
+
+ &[labeldirection=rtl]:not([pinned]),
+ &:not([labeldirection], [pinned]):-moz-locale-dir(rtl) {
+ direction: rtl;
+ mask-image: linear-gradient(to right, transparent, black var(--tab-label-mask-size));
+ }
+ }
+}
+
.tab-label {
margin-inline: 0;
+ line-height: 1.7; /* override 'normal' in case of fallback math fonts with huge metrics */
+ white-space: nowrap;
}
.tab-close-button {
@@ -393,6 +465,11 @@
padding: 6px;
border-radius: var(--tab-border-radius);
list-style-image: url(chrome://global/skin/icons/close-12.svg);
+
+ &[pinned],
+ #tabbrowser-tabs[closebuttons="activetab"] > #tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-content > &:not([selected]) {
+ display: none;
+ }
}
/* The following rulesets allow showing more of the tab title */
@@ -407,15 +484,24 @@
}
-#tabbrowser-tabs:not([secondarytext-unsupported]) .tab-label-container {
- height: 2.7em;
-}
-
.tab-secondary-label {
font-size: .75em;
+ margin: -.3em 0 .3em; /* adjust margins to compensate for line-height of .tab-label */
opacity: .8;
- #tabbrowser-tabs[secondarytext-unsupported] & {
+ #tabbrowser-tabs[secondarytext-unsupported] &,
+ :root[uidensity=compact] &,
+ &:not([soundplaying], [muted], [activemedia-blocked], [pictureinpicture]),
+ &:not([activemedia-blocked]) > .tab-icon-sound-blocked-label,
+ &[muted][activemedia-blocked] > .tab-icon-sound-blocked-label,
+ &[activemedia-blocked] > .tab-icon-sound-playing-label,
+ &[muted] > .tab-icon-sound-playing-label,
+ &[pictureinpicture] > .tab-icon-sound-playing-label,
+ &[pictureinpicture] > .tab-icon-sound-muted-label,
+ &:not([pictureinpicture]) > .tab-icon-sound-pip-label,
+ &:not([muted]) > .tab-icon-sound-muted-label,
+ &:not([showtooltip]) > .tab-icon-sound-tooltip-label,
+ &[showtooltip] > .tab-icon-sound-label:not(.tab-icon-sound-tooltip-label) {
display: none;
}
@@ -474,8 +560,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;
}
@@ -496,10 +582,9 @@
/* Pinned tabs */
.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));
+.tabbrowser-tab > .tab-stack > .tab-content[pinned][titlechanged]:not([selected]) {
+ 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..21a6d216ed 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,
@@ -115,13 +114,9 @@
position: relative;
}
-#nav-bar-customization-target :where(#reload-button, #stop-button) > .toolbarbutton-icon {
- padding: calc(var(--toolbarbutton-inner-padding) + 1px) var(--toolbarbutton-inner-padding) calc(var(--toolbarbutton-inner-padding) - 1px ) !important; /* The animation is 18px but the other icons are 16px, lower it by 1px so it is vertically centered */
-}
-
#reload-button > .toolbarbutton-animatable-box,
#stop-button > .toolbarbutton-animatable-box {
- top: calc(50% - 9px); /* Vertically center the 20px tall animatable image, which is 1px higher than other icons */
+ top: calc(50% - 10px); /* Vertically center the 20px tall animatable image */
width: 20px; /* Width of each frame within the SVG sprite */
height: 20px; /* Height of each frame within the SVG sprite */
}
@@ -452,10 +447,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");
}
@@ -471,3 +462,9 @@ toolbarbutton.bookmark-item {
#firefox-view-button {
list-style-image: url("chrome://browser/skin/firefox-view.svg");
}
+
+#content-analysis-indicator {
+ -moz-context-properties: fill, stroke;
+ stroke: var(--toolbarbutton-icon-fill);
+ list-style-image: url("chrome://global/skin/icons/content-analysis.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..716ec02def 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;
}
@@ -63,11 +63,19 @@ h1.translations-panel-header-wrapper {
/* The default styling is to dim the default, but here override it so that it still uses
the primary color. */
-.translations-panel-footer > button[default][disabled="true"] {
+.translations-panel-button-group > button[default][disabled="true"] {
color: var(--button-primary-color);
background-color: var(--button-primary-bgcolor);
}
+#select-translations-panel-footer {
+ display: flex;
+ flex: 1;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: var(--space-small);
+}
+
#full-page-translations-panel-translate-hint-action {
appearance: none;
background-color: var(--button-bgcolor);
@@ -117,44 +125,70 @@ h1.translations-panel-header-wrapper {
padding: 12px;
}
-.select-translations-panel-button {
- align-items: center;
- justify-content: center;
- margin-inline: 0;
-}
-
.select-translations-panel-content {
padding: var(--arrowpanel-padding);
padding-block: 4px;
}
.select-translations-panel-copy-button {
+ list-style-image: url(chrome://global/skin/icons/edit-copy.svg);
background-color: transparent;
- font: message-box;
- font-weight: var(--font-weight-bold);
- &::before {
- content: url(chrome://global/skin/icons/edit-copy.svg);
- fill: currentColor;
- margin-inline-end: 5px;
- -moz-context-properties: fill;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ margin: 0;
+
+ > .button-box {
+ gap: var(--space-xsmall);
+ }
+
+ &.copied {
+ list-style-image: url(chrome://global/skin/icons/check.svg);
}
}
+#select-translations-panel-copy-button {
+ justify-content: flex-start;
+}
+
+#select-translations-panel-footer-button-group {
+ flex: 1;
+ justify-content: flex-end;
+}
+
.select-translations-panel-header {
padding: var(--arrowpanel-padding);
text-align: initial;
}
-.select-translations-panel-label {
- margin-inline: 2px;
+.select-translations-panel-label,
+.select-translations-panel-message-bar {
+ margin-block: 0 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;
+ resize: vertical;
+
+ &: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..4362c19d5d 100644
--- a/browser/themes/shared/urlbarView.css
+++ b/browser/themes/shared/urlbarView.css
@@ -52,11 +52,15 @@
--urlbarView-labeled-row-label-top: calc(-1.27em - 2px);
--urlbarView-labeled-tip-margin-top-extra: 8px;
+ --urlbarView-action-button-background-color: light-dark(white, #1C1B22);
+ --urlbarView-action-button-hover-color: light-dark(#5B5B66, var(--buttons-destructive-color));
+ --urlbarView-action-button-selected-color: light-dark(#1C1B22, var(--urlbarView-action-button-background-color));
+
&:-moz-locale-dir(rtl) {
--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 +615,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 +715,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;
}
}
@@ -1039,3 +1031,43 @@
background: var(--urlbarView-highlight-background);
color: var(--urlbarView-highlight-color);
}
+
+.urlbarView-row:has(.urlbarView-actions-container:not(:empty)) {
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.urlbarView-actions-container {
+ margin-inline-start: calc(var(--urlbarView-item-inline-padding) + var(--urlbarView-favicon-margin-start) + var(--urlbarView-favicon-width) + var(--urlbarView-icon-margin-end));
+ margin-block-end: var(--urlbarView-item-block-padding);
+}
+
+.urlbarView-action-btn {
+ font-size: smaller;
+ color: var(--toolbar-field-focus-color);
+ border-radius: var(--toolbarbutton-border-radius);
+ border: 1px solid transparent;
+ padding: .4em .6em;
+ display: inline-flex;
+ align-items: center;
+ background-color: var(--urlbarView-action-button-background-color);
+ box-shadow: 0 0px 4px rgba(0, 0, 0, 0.23);
+}
+
+.urlbarView-action-btn img {
+ width: 16px;
+ height: 16px;
+ margin-inline-end: .4em;
+ -moz-context-properties: fill, fill-opacity;
+}
+
+.urlbarView-action-btn:hover {
+ color: var(--urlbarView-result-button-hover-color);
+ background-color: var(--urlbarView-action-button-hover-color);
+}
+
+.urlbarView-action-btn[selected] {
+ color: light-dark(var(--urlbarView-result-button-hover-color), var(--toolbar-field-focus-color));
+ background-color: var(--urlbarView-action-button-selected-color);
+ border-color: light-dark(transparent, white);
+}
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..00a8f5d318 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);
@@ -154,6 +154,11 @@ async function clickOn(selector, beforeContentFn) {
await SpecialPowers.spawn(lastTab.linkedBrowser, [], beforeContentFn);
}
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ browserWindow.PopupNotifications.panel,
+ "popupshown"
+ );
+
await SpecialPowers.spawn(lastTab.linkedBrowser, [selector], arg => {
/* eslint-env mozilla/chrome-script */
let element = content.document.querySelector(arg);
@@ -161,8 +166,5 @@ async function clickOn(selector, beforeContentFn) {
});
// Wait for the popup to actually be shown before making the screenshot
- await BrowserTestUtils.waitForEvent(
- browserWindow.PopupNotifications.panel,
- "popupshown"
- );
+ await popupShownPromise;
}
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);
});
},
diff --git a/browser/tools/mozscreenshots/permissionPrompts/browser.toml b/browser/tools/mozscreenshots/permissionPrompts/browser.toml
index 0ddc6302ed..96e59c1b6b 100644
--- a/browser/tools/mozscreenshots/permissionPrompts/browser.toml
+++ b/browser/tools/mozscreenshots/permissionPrompts/browser.toml
@@ -7,4 +7,3 @@ prefs = [
]
["browser_permissionPrompts.js"]
-skip-if = ["os == 'mac'"] # times out on macosx1014, see 1570098